96 lines
6.6 KiB
Markdown
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.
|