ccr-platform / deploy /PRODUCTION_RUNBOOK.md
devaanand's picture
chore: add MIT LICENSE and production deploy assets
4d1197e
|
Raw
History Blame Contribute Delete
11.1 kB
# Production Deployment Runbook
Moving the CCR Platform from the personal dev setup to the lab's own accounts,
and putting it on a custom domain. Written for a one-sitting migration: the code
is already unified in the lab GitHub repo, the app is feature-complete, and data
already lives on managed services (Postgres + object storage). This runbook
carries all of it over to lab-owned accounts.
Terminology: "the Space" is the Hugging Face Space that runs the app. "Custom
domain" is whatever the PI picks (examples below use `ccr.culturemoralitylab.org`;
swap in the chosen domain everywhere).
There are no em dashes in this file on purpose (project rule).
---
## 0. What is already done
- Code is unified in `Culture-and-Morality-Lab/ccr-platform` (public), with clean
commit history.
- License texts are staged in the repo root (`LICENSE-MIT`, `LICENSE-APACHE`);
one gets promoted to `LICENSE` once the PI picks.
- The app is one portable container (`Dockerfile`); the same image runs on any
host, so "deploy" is a git push to the Space.
- Data services are already in use on the dev instance (Supabase Postgres via
`DATABASE_URL`, Cloudflare R2 via the `CCR_S3_*` secrets), so this is a
copy-to-lab-accounts job, not new infrastructure.
## 1. Accounts to create (lab email, free)
| Service | Purpose | Cost |
| --- | --- | --- |
| Hugging Face (account or org) | Hosts the Space | Free (cpu-basic) |
| Supabase | Postgres database + Google sign-in | Free tier |
| Cloudflare | Domain registrar + DNS + R2 storage + the reverse-proxy Worker | Free (domain ~20/yr; R2 needs a card on file but is free at this scale) |
| Groq | AI item-drafting key | Free tier |
| GitHub | Already done (lab org repo exists) | Free |
Save every password and key in a password manager as you go. These are the lab's
institutional credentials, not personal logins.
## 2. Secrets sheet (fill privately, never commit)
The new Space needs these. Collect the values as you complete each section
below, then paste them into the Space in section 7.
| Name | Store | Value source |
| --- | --- | --- |
| `CCR_SESSION_SECRET` | Secret | Generate fresh: `python3 -c "import secrets; print(secrets.token_hex(32))"` (do NOT reuse the dev value) |
| `DATABASE_URL` | Secret | New Supabase project, Session pooler URI, with the DB password in it (section 3) |
| `SUPABASE_URL` | Secret | New Supabase project > Settings > API |
| `SUPABASE_ANON_KEY` | Secret | Same page (anon public key, NOT service_role) |
| `CCR_STORAGE` | Variable | `s3` |
| `CCR_S3_ENDPOINT` | Secret | New R2: `https://<account_id>.r2.cloudflarestorage.com` (section 5) |
| `CCR_S3_BUCKET` | Secret | New R2 bucket name |
| `CCR_S3_ACCESS_KEY_ID` | Secret | New R2 API token |
| `CCR_S3_SECRET_ACCESS_KEY` | Secret | New R2 API token |
| `GROQ_API_KEY` | Secret | New lab Groq key |
| `CCR_APP_URL` | Variable | `https://<custom-domain>` (section 8) |
| `CCR_COOKIE_SECURE` | Variable | `1` |
| `CCR_ANON_TTL_HOURS` | Variable | `24` |
| `ADMIN_EMAILS` | Variable | Comma-separated admin emails (confirm with PI) |
| `CCR_MAX_ROWS` | Variable | `50000` (matches the dev instance) |
Hugging Face keeps Variables and Secrets in two separate stores. A name defined
in BOTH puts the Space into CONFIG_ERROR before it builds. Put each name in one
store only, per the table.
## 3. Supabase project (database + sign-in)
1. New project under the lab's Supabase org. Match the region to the current
dev project (read it from the dev `DATABASE_URL`: the pooler host contains it,
e.g. `aws-0-us-east-1...` means US East). Generate a strong DB password and
save it.
2. Turn OFF "Enable Data API" (the app talks to Postgres directly and never uses
the REST API; leaving it off closes that surface). Leave "Enable automatic
RLS" on. Do NOT connect GitHub (the app manages its own schema).
3. Settings > API: copy the Project URL and the anon key into the secrets sheet
(`SUPABASE_URL`, `SUPABASE_ANON_KEY`).
4. Connect > Session pooler: copy the URI, insert the DB password, into
`DATABASE_URL`.
5. Google sign-in (do this before launch; the button is hidden without it):
- Google Cloud console (lab Google account) > OAuth consent screen (External)
> Credentials > Create OAuth client ID (Web application).
- Supabase > Authentication > Providers > Google > Enable. Copy the shown
callback (`https://<PROJECT_REF>.supabase.co/auth/v1/callback`) and add it
as an authorized redirect URI in the Google client. Paste the Google client
id and secret back into the Supabase Google form.
- Supabase > Authentication > URL Configuration: add the redirect URLs
`http://127.0.0.1:8000/api/auth/google/callback` and, once the domain is
live, `https://<custom-domain>/api/auth/google/callback`.
## 4. Migrate the database (carry accounts and data over)
Run BEFORE the new Space boots for the first time, so the app finds the data
already present rather than creating empty tables.
```sh
# Dump the app's tables from the current (dev) database. Use the dev
# DATABASE_URL from your local .env. --schema=public keeps it to the app's
# tables; --no-owner --no-acl avoids role mismatches between projects.
pg_dump "$DEV_DATABASE_URL" --schema=public --no-owner --no-acl -Fc -f ccr_public.dump
# Restore into the NEW Supabase project.
pg_restore --no-owner --no-acl --clean --if-exists \
-d "$NEW_DATABASE_URL" ccr_public.dump
```
If `pg_dump` version-mismatches against Supabase, use the Postgres client that
matches the server major version (or the `supabase db dump` CLI). The app's
startup auto-migration adds any missing columns after this, so a slightly older
dump still boots cleanly.
## 5. R2 object storage (uploaded files)
1. Cloudflare > R2 > create a bucket under the lab account (this is when R2 asks
for a card on file; the 10 GB free tier covers this use).
2. Create an R2 API token (Object Read and Write) scoped to that bucket. Put the
access key id, secret, endpoint, and bucket name into the secrets sheet.
3. Copy the existing files over (R2 is S3-compatible):
```sh
# From the dev R2 to the lab R2. Fill both endpoint/keys from each account.
aws s3 sync \
--endpoint-url "$DEV_S3_ENDPOINT" "s3://$DEV_BUCKET" ./r2-migrate-tmp
aws s3 sync \
--endpoint-url "$LAB_S3_ENDPOINT" ./r2-migrate-tmp "s3://$LAB_BUCKET"
# (or use rclone with two remotes: rclone sync dev:bucket lab:bucket)
```
## 6. Domain purchase (on the PI call, card needed)
1. Cloudflare > Domain Registration > register the chosen domain (recommended:
buy at Cloudflare so domain, DNS, R2, and the Worker share one account).
Availability was last checked recently and can change daily, so register the
same day it is chosen.
2. If the domain is bought elsewhere, add it to Cloudflare and point its
nameservers at Cloudflare, so the Worker route in section 8 can attach.
## 7. Create the lab Space and add secrets
1. New Space under the lab Hugging Face account: SDK = Docker, hardware =
cpu-basic (free, same as dev). Give it the same repo.
2. Point the local repo's Space remote at the new Space and push:
```sh
git remote set-url hf https://huggingface.co/spaces/<lab-owner>/<space>
git push hf main # use the lab HF username + a WRITE token when prompted
```
3. Space > Settings > Variables and secrets: enter every row from the secrets
sheet (section 2), each in the store the table specifies.
4. The Space builds and boots. Because the database was restored in section 4,
accounts and data are already there.
## 8. Wire the custom domain (reverse proxy)
Hugging Face Spaces cannot serve a custom domain directly, so a small Cloudflare
Worker serves the Space at the lab domain and keeps the domain in the address bar.
1. Get the Space direct host: Space > Settings > Embed this Space > Direct URL,
of the form `https://<owner>-<space>.hf.space`.
2. Open `deploy/reverse-proxy-worker.js`, set `UPSTREAM_HOST` to that host
(without the `https://`).
3. Cloudflare > Workers & Pages > Create Worker > paste the file > Deploy.
4. That Worker > Settings > Domains & Routes > Add Custom Domain >
`ccr.<domain>` (Cloudflare provisions TLS automatically).
5. Set the Space Variable `CCR_APP_URL=https://ccr.<domain>` and confirm the
Supabase redirect URL from section 3.5 uses the same domain. Restart the
Space so it picks up `CCR_APP_URL`.
## 9. Launch checklist (verify before announcing)
- Site loads on the custom domain; the address bar stays on the domain while
navigating.
- Google sign-in completes and returns to the custom domain (not hf.space).
- Password sign-in works; an account created on the dev instance is present
(confirms the data migration).
- Upload a small corpus, pick a construct, run it, open results.
- AI drafting: "Draft with AI" tab works with the lab Groq key; the drafted
items appear, and a saved AI construct shows the "AI-generated, not validated"
label through picker, results, and the metadata download.
- `/guide` loads; `/admin` is reachable for the emails in `ADMIN_EMAILS`.
- Promote the license: `git mv LICENSE-MIT LICENSE && rm LICENSE-APACHE` (or the
reverse), commit, push. Needed because the site publicly says the tool is
open-source.
## 10. Cut over and retire the old instance
- Pause (do not delete) the old personal Space once the new one is verified, so
it is a fallback for a day or two.
- Keep the dev database and R2 until the new instance has run cleanly for a few
days, then retire them.
---
## Optional, post-launch
### Keep the Space awake (free)
Free Spaces sleep after about 48 hours idle (first visitor then waits about a
minute). A free uptime pinger (for example UptimeRobot) hitting the domain every
few hours keeps it warm without paying. The durable fix is an always-on host
(next item).
### Always-on host (later)
The same container runs on any host (Fly, Railway, Hetzner, a UMass VM). Moving
there removes the sleep behavior AND gives native custom-domain support, so the
Worker in section 8 can be dropped. Not a launch blocker.
### One-push deploys (GitHub Action)
Auto-deploy the Space on every merge to `main`, so nobody needs the Space remote.
Add a GitHub Actions workflow that mirrors `main` to the Space, with an HF write
token stored as a GitHub Actions secret (`HF_TOKEN`):
```yaml
# .github/workflows/deploy-hf.yml
name: Deploy to Hugging Face Space
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Push to Space
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
git push "https://lab:${HF_TOKEN}@huggingface.co/spaces/<lab-owner>/<space>" main
```
### Switch AI drafting to Claude Haiku (later)
Add `ANTHROPIC_API_KEY` as a Space secret and the app auto-selects Anthropic over
Groq (`CCR_GENERATION_PROVIDER` can force either). Groq stays as the free
fallback. The provenance stamp records which model drafted each construct, so
constructs drafted during the Groq period stay traceable.