Add deployment: Dockerfile, compose, proxy config, and docs

One container plus a Postgres behind any TLS-terminating proxy. Nothing is
specific to a particular host.

The app and API are served from a SINGLE origin. This is not tidiness: browser
auth sessions live in per-origin storage, so splitting them across two
hostnames makes sign-in loop in a way that presents as a server fault. The
short alias redirects rather than serving a second origin.

Two safety properties verified by running the image, not by reading the code:

- With NODE_ENV=production and no SUPABASE_URL, the process refuses to start
  and says why. Serving the whole CRM unauthenticated is a worse outcome than
  failing to deploy, so the failure is deliberate and loud.
- In production the development auth bypass does not apply: an unauthenticated
  request to /api/dashboard returns 401 rather than adopting the first user in
  the table.

The Dockerfile typechecks all six packages as a build gate, so a deploy that
does not compile fails at build time rather than in front of a user. Runtime
runs unprivileged as `node`, and Postgres is not published to the host.

Docs cover the ontology and why it is shaped this way, agent connection for
Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing
seed data about real people — including how to have your record removed.

Verified: image builds, container reports healthy, serves the SPA, enforces
auth, and the production guard exits non-zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:19:53 -07:00
parent de33a03524
commit c747eb2aa7
7 changed files with 456 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Caddy — reverse proxy for PIG.
#
# Serve the app and the API from ONE hostname. Auth sessions live in
# per-origin browser storage, so splitting them across two hostnames makes
# sign-in loop endlessly in a way that looks like a server fault.
primeintellectgrowth.com, www.primeintellectgrowth.com {
encode zstd gzip
# The MCP endpoint, when Streamable HTTP is enabled. Same origin as the
# app so it shares the session and needs no CORS allowance.
reverse_proxy 127.0.0.1:8920
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
# The app is entirely first-party except for the auth provider, which
# it must reach over XHR.
Content-Security-Policy "default-src 'self'; connect-src 'self' https://*.supabase.co; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-ancestors 'none'; base-uri 'self'"
-Server
}
}
# Short alias. A redirect rather than a second origin, deliberately — see above.
pig.karti.ai {
redir https://primeintellectgrowth.com{uri} permanent
}
+76
View File
@@ -0,0 +1,76 @@
# Deploying PIG
PIG is one container plus a 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 |
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only),
`PIGGY_ENABLED` + `ANTHROPIC_API_KEY`, and the Slack and Buzz credentials.
## 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
```
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.
## 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
```
## 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`.