Files
Jyotisha/docs/operations/self-hosted-identity.md
T
Jesse_Chen 9d8c73561f
Staging Backend Quality Gate / validate (push) Failing after 7m30s
Staging Backend Quality Gate / publish (push) Has been skipped
test(deploy): integrate billing admin rollout checks
2026-08-06 20:15:50 +08:00

96 lines
6.6 KiB
Markdown

# Self-hosted identity operations
Staging uses Better Auth and the private local PostgreSQL cluster for both identity and business data. Production remains on Supabase; this runbook does not authorize a production cutover.
## Staging mode
Keep these values exactly as shown:
```dotenv
AUTH_PROVIDER=self-hosted
SELF_HOSTED_IDENTITY_ENABLED=true
AUTH_USER_ORIGIN=https://staging.jyotisha.chat
ADMIN_USER_ORIGIN=https://admin.staging.jyotisha.chat
```
The two exact hosts run the same Next.js application and Better Auth service. They share one `BETTER_AUTH_USER_SECRET` and cookie prefix, but Better Auth cookies do not set a `Domain` attribute, so the browser keeps main-site and admin-site sessions host-only. The admin host root redirects to `/admin`, and its login page returns to `/admin` after OTP, password, or MFA completion. The main host login continues to return to `/`. The main auth surface still returns `404` for Better Auth `/api/auth/admin` plugin endpoints; the exact admin host may dispatch those endpoints to the same service. Unknown, suffix-spoofed, malformed, or unconfigured hosts fail closed with `421`.
Server routes translate the authenticated user session into PostgreSQL request claims. Admin authorization reads the persisted `identity.users.role` value and permits only `admin`; `viewer` and ordinary users receive `403` regardless of which host receives the request.
Use [the tracked staging identity example](../../deploy/.env.staging.identity.example) as a list of names only. Replace bracketed values directly on the server. Keep both `/opt/jyotisha-staging/.env.staging` and `/opt/jyotisha-staging/.env.staging.database` on the staging host, owned by the deployment user, and at mode `0600`.
Generate `BETTER_AUTH_USER_SECRET` locally on the server:
```bash
openssl rand -base64 32
```
Do not reuse it as a PostgreSQL password. `IDENTITY_DATABASE_URL`, `APP_DATABASE_URL`, `SERVICE_DATABASE_URL`, and `ADMIN_DATABASE_URL` use their matching passwords from `.env.staging.database`, percent-encoded only in each URL password component. All four must point to the private Compose hostname `postgres:5432/jyotisha`; never publish PostgreSQL on a host port.
For the service identity, generate one independent secret and store it in exactly these two host-managed forms:
- `.env.staging.database`: raw `SERVICE_RUNTIME_PASSWORD=<generated>` for PostgreSQL role bootstrap and compatibility validation.
- `.env.staging`: `SERVICE_DATABASE_URL=postgresql://service_runtime:<percent-encoded-service-runtime-password>@postgres:5432/jyotisha` for server runtime access.
The staging deploy controller disables shell tracing and runs both environment validators before any Compose config, pull, or up operation. Neither staging workflow stores, interpolates, passes, or prints `SERVICE_RUNTIME_PASSWORD` or `SERVICE_DATABASE_URL`; they remain in the mode-`0600` host files.
The Resend key must be staging-only. `RESEND_FROM_EMAIL` must use a sender/domain verified in Resend. CI never receives this key and uses an in-memory sender. `ADMIN_EMAILS` remains relevant only to the legacy Supabase production path; it is not self-hosted admin authorization.
Validate without printing values:
```bash
cd /opt/jyotisha-staging
chmod 600 .env.staging .env.staging.database
bash deploy/validate-staging-env.sh \
.env.staging staging.jyotisha.chat deploy/Caddyfile.staging
bash deploy/validate-staging-database-env.sh .env.staging.database
```
## Migration and smoke checks
Apply the reviewed PostgreSQL migrations through the existing `Migrate Staging Database` workflow before deploying the web image. Better Auth users are transactionally projected into `auth.users`, which creates their business profile through the existing trigger.
After deployment, verify both exact hosts without sending credentials:
```bash
curl -fsS https://staging.jyotisha.chat/login >/dev/null
test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/api/account)" = 401
test "$(curl -sS -o /dev/null -w '%{http_code}' https://admin.staging.jyotisha.chat/)" = 308
test "$(curl -sSI https://admin.staging.jyotisha.chat/ | tr -d '\r' | awk 'tolower($1) == "location:" { print $2 }')" = /admin
curl -fsS https://admin.staging.jyotisha.chat/login >/dev/null
test "$(curl -sS -o /dev/null -w '%{http_code}' https://admin.staging.jyotisha.chat/api/admin/session)" = 401
```
An anonymous admin-host `/admin` request redirects to the admin-host `/login`. An authenticated non-admin, including a persisted `viewer`, must not render the admin layout and every `/api/admin/*` route must independently return `403`. Promote a staging user only through a reviewed database operation; the persisted role must include `admin` before the admin-host session can enter the backend. Complete MFA enrollment, MFA login, and privileged reauthentication on `admin.staging.jyotisha.chat` so every resulting challenge, session, and reauth cookie remains on the admin host.
## Optional import rehearsal
Export Supabase Auth users to a JSON array in the supported fixture shape, then run a redacted dry-run first:
```bash
cd /opt/jyotisha-staging/frontend
node scripts/import-supabase-auth-users.mjs /secure/path/auth-users.json
```
The summary contains only counts. It preserves UUID, normalized email, verification timestamps, display metadata, and created/updated timestamps. It intentionally ignores passwords, sessions, JWTs, provider secrets, and Supabase platform fields.
Apply only after reviewing the dry-run and taking a local encrypted staging backup:
```bash
set -a
. ../.env.staging
set +a
node scripts/import-supabase-auth-users.mjs /secure/path/auth-users.json --apply
unset IDENTITY_DATABASE_URL
```
Reruns are idempotent by UUID and the whole import is transactional. Duplicate canonical emails abort before database writes.
## Rollback and rotation
An application rollback must use a previously validated staging image and does not reverse database migrations. Existing self-hosted sessions and data remain in PostgreSQL; do not delete identity or business rows during application rollback. Returning staging to Supabase requires a separate reviewed data-reconciliation and provider-switch change.
Rotating `BETTER_AUTH_USER_SECRET` invalidates all self-hosted browser sessions, including admins. Restart the web service and verify login, anonymous admin API rejection, admin access, and viewer rejection. Rotate a leaked Resend key in Resend first, replace the server value, then restart. Never print old or new values.
Production `AUTH_PROVIDER=self-hosted` remains blocked until data reconciliation passes, production backups and restore drills exist, operational monitoring is ready, and a separate reviewed production cutover plan is approved.