c821b2ca07
CI / verify (push) Successful in 2m55s
The seam existed with only a Supabase implementation, so an on-prem deployment had no way to authenticate. A customer running PIG inside their own network already has Okta, Entra, Keycloak, Auth0 or Google Workspace; asking them to stand up a second identity system is a serious adoption tax and in a regulated environment usually refused outright. Setting PIG_OIDC_ISSUER is normally the whole configuration — the JWKS is discovered from the issuer's well-known document. PIG_OIDC_JWKS_URI skips discovery entirely for an air-gapped network. OIDC takes precedence over Supabase so an on-prem install can leave the hosted values in its environment file without them quietly taking over. Three decisions worth stating: Discovery is resolved lazily and the FAILURE is not cached. Doing it per request would put the customer's identity provider on the critical path of every API call; doing it eagerly at boot would mean their IdP rebooting takes the CRM down with it. So it happens on first use and retries on the next request. The audience check is optional but warned about loudly. Without it, a token the provider issued for ANY other application in the same tenant verifies here — a token minted for an unrelated internal tool would be accepted as a PIG session. It cannot be mandatory because some providers legitimately issue single-audience tokens. Email falls back through email, preferred_username and upn, because providers disagree, but a preferred_username without an "@" is ignored — PIG keys membership on the address, and a bare username must never become an account identity. Also fixed a warning that claimed "authentication is DISABLED" on a correctly configured OIDC deployment. That is worse than silence: an operator who reads it on a secure install learns to ignore the warnings. The dev bypass itself was already correct — it keys on the resolved provider rather than on Supabase. 18 new tests, most of them about what the provider must REFUSE: a foreign signing key, a foreign issuer, a token for a different application, an expired token, a token with no subject, and a discovery outage that must not become permanent. Keys are generated per test and the JWKS is served locally, so they run offline. Verified: production refuses to start with neither provider, starts with OIDC alone, enforces 401 on an unauthenticated request, and warns only about the genuinely missing admin list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Markdown
135 lines
5.1 KiB
Markdown
# Deploying PIG
|
|
|
|
PIG is an API/web container, a private Piggy worker/chat container and Postgres,
|
|
behind any reverse proxy that terminates TLS. Nothing here is specific to a
|
|
particular host.
|
|
|
|
## 1. DNS
|
|
|
|
Point the apex and `www` at the machine. Both must resolve before the proxy can
|
|
obtain a certificate.
|
|
|
|
```
|
|
A primeintellectgrowth.com -> <your IP>
|
|
A www.primeintellectgrowth.com -> <your IP>
|
|
```
|
|
|
|
## 2. Configuration
|
|
|
|
```bash
|
|
cp .env.example .env # then edit
|
|
```
|
|
|
|
The values that must be set for a production start:
|
|
|
|
| Variable | Why |
|
|
|---|---|
|
|
| `POSTGRES_PASSWORD` | Generate a fresh one; never reuse another service's |
|
|
| `PIG_PUBLIC_URL` | The single origin the app is served from |
|
|
| `SUPABASE_URL` / `SUPABASE_ANON_KEY` | Authentication. The app refuses to start in production without a Supabase URL, because it would otherwise serve the whole CRM unauthenticated |
|
|
| `PIG_ADMIN_EMAILS` | Who may administer. **Every address here must already have an account** — an unregistered address listed as an admin is a standing offer of admin rights to whoever claims it first |
|
|
| `PIGGY_INFERENCE_API_KEY` | Model credential held only by the Piggy process |
|
|
| `PIGGY_INTERNAL_TOKEN` | A generated 32+ character bearer token shared only by API and Piggy |
|
|
|
|
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only),
|
|
`PIGGY_ENABLED`, and the Slack and Buzz credentials.
|
|
|
|
Piggy listens on `piggy:8931` inside the Compose network. The port is exposed to
|
|
other containers but never published to the host, and Caddy must not route to
|
|
it. The CRM API authenticates the user, forwards only bounded chat context, and
|
|
uses `PIGGY_INTERNAL_TOKEN` in an Authorization header. Never put that token in
|
|
a query string, where proxies and access logs can retain it.
|
|
|
|
## 3. Start
|
|
|
|
```bash
|
|
docker compose -p pig up -d --build
|
|
docker compose -p pig exec app npx tsx packages/db/src/migrate.ts
|
|
docker compose -p pig exec app npx tsx packages/db/src/seed/index.ts # optional
|
|
```
|
|
|
|
When Piggy is enabled, start its private profile as well:
|
|
|
|
```bash
|
|
docker compose -p pig --profile piggy up -d --build
|
|
```
|
|
|
|
Use `-p pig`. A compose project that shares a name with a neighbouring stack
|
|
will adopt its volumes, which is a memorable way to lose a database.
|
|
|
|
## 4. Reverse proxy
|
|
|
|
See `Caddyfile.example`. Serve the app and API from the **same** origin.
|
|
|
|
Two things that will otherwise cost you an hour:
|
|
|
|
- **If other sites on the host use `bind <address>`, yours must too.** Caddy
|
|
groups site blocks into servers by listen address. A block without `bind`
|
|
lands in a *separate* server on `:443`, and the more specific listener wins
|
|
for traffic arriving on that address — which is all public traffic after NAT.
|
|
The symptom is a valid certificate, a 200 response, an empty body, and none
|
|
of your headers. It looks like the app is broken; it is that the request
|
|
never reached it.
|
|
|
|
- **The CSP must carry the hash of the inline theme script** in `index.html`.
|
|
That script sets light or dark before first paint so dark-mode users do not
|
|
get a white flash. Editing it changes the hash and CSP will silently block
|
|
it — the browser console prints the hash it expects.
|
|
|
|
## 5. Verify
|
|
|
|
```bash
|
|
curl -s https://primeintellectgrowth.com/api/health
|
|
# {"ok":true,"service":"pig","version":"0.1.0"}
|
|
```
|
|
|
|
## Upgrading
|
|
|
|
```bash
|
|
git pull
|
|
docker compose -p pig up -d --build
|
|
docker compose -p pig exec app npx tsx packages/db/src/migrate.ts
|
|
```
|
|
|
|
Migrations are additive and safe to re-run; Drizzle tracks what has been
|
|
applied. Take a dump before a major upgrade anyway:
|
|
|
|
```bash
|
|
docker compose -p pig exec db pg_dump -U pig pig | gzip > pig-$(date +%F).sql.gz
|
|
```
|
|
|
|
## On-premises: using your own identity provider
|
|
|
|
PIG authenticates against any standards-compliant OIDC provider, which is how
|
|
an install inside your own network works. Set:
|
|
|
|
```bash
|
|
PIG_OIDC_ISSUER=https://id.yourcompany.internal
|
|
PIG_OIDC_AUDIENCE=pig # the client/app id you registered for PIG
|
|
```
|
|
|
|
That is usually the whole configuration — the JWKS is discovered from the
|
|
issuer. On an air-gapped network, set `PIG_OIDC_JWKS_URI` too and no discovery
|
|
request is made.
|
|
|
|
`PIG_OIDC_ISSUER` takes precedence over `SUPABASE_URL`, so the hosted values
|
|
can stay in the environment file without quietly taking over.
|
|
|
|
**Set the audience.** Without it, any token your provider issued for any
|
|
application in the same tenant verifies here — a token minted for an unrelated
|
|
internal tool would be accepted as a PIG session. PIG warns about this at boot
|
|
but cannot refuse, because some providers legitimately issue single-audience
|
|
tokens.
|
|
|
|
**Provisioning stays in PIG.** Authenticating proves who someone is; it does
|
|
not make them a member. They still need an invite, and their team and role live
|
|
in PIG's database. That is deliberate — your directory should not have to model
|
|
"supply lead versus demand member" for one application.
|
|
|
|
## A note on the auth project
|
|
|
|
PIG verifies JWTs but authorizes from its own `users` table. If the Supabase
|
|
project is shared with another application, its users get **nothing** here until
|
|
they are explicitly invited. That is deliberate, and it is why a valid token can
|
|
still return `403 needs_profile`.
|