The app is pre-launch and shared by link with a handful of people at Prime
Intellect. It should not be accumulating a search footprint yet.
Three layers, because each covers a gap the others leave:
- robots.txt asks well-behaved crawlers not to fetch at all.
- The <meta name="robots"> tag covers the HTML document for anything that
fetched anyway.
- X-Robots-Tag covers everything that is NOT the HTML document — og.png,
the manifest, the built assets — which the meta tag cannot reach.
noarchive and nosnippet are there so a cache or an excerpt cannot outlive
the page once this is reversed.
Deliberately NOT stripped: the og:/twitter: tags. Link unfurlers are not
crawlers — they fetch on behalf of the person pasting the link, and a
rendered card is exactly what we want when this is shared.
The real gate remains authentication: / returns the sign-in screen and every
/api/ route returns 401. This only stops the app being indexed.
To go public: delete robots.txt, drop the meta tag, drop the header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
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
docker compose -p pig up -d --build
docker compose -p pig exec app pnpm exec tsx packages/db/src/migrate.ts
docker compose -p pig exec app pnpm exec tsx packages/db/src/seed/index.ts # optional
When Piggy is enabled, start its private profile as well:
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 withoutbindlands 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
curl -s https://primeintellectgrowth.com/api/health
# {"ok":true,"service":"pig","version":"0.1.0"}
Upgrading
git pull
docker compose -p pig up -d --build
docker compose -p pig exec app pnpm exec 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:
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:
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.