Spaces:
Running
Deploying the lab instance (Hugging Face Space)
The Space builds from this repo's Dockerfile. One-time setup lives in the
Space settings; after that, deploys are just git push prod main.
This repo deploys to the Space Culture-and-Morality-Lab/ccr-platform, which
is served to the public at https://psychologicaltextanalysis.com through the
Cloudflare Worker in deploy/reverse-proxy-worker.js (Spaces cannot hold a
custom domain on any tier). Account-by-account setup and the migration from the
personal dev stack live in deploy/PRODUCTION_RUNBOOK.md.
Space settings (Settings > Variables and secrets)
Hugging Face keeps Variables and Secrets in two separate stores, and a
name defined in BOTH puts the Space into CONFIG_ERROR ("Collision on
variables and secrets names") before it even builds. Add each key below to one
store only - if the Space reports a config error after a settings change, look
for a duplicated name first, not a bad value.
Secrets (credentials - encrypted, write-only once set):
| Secret | Value |
|---|---|
| CCR_SESSION_SECRET | python3 -c "import secrets; print(secrets.token_hex(32))" |
| SUPABASE_URL | from Supabase > Project Settings > API |
| SUPABASE_ANON_KEY | from the same page (anon public key, NOT service_role) |
| DATABASE_URL | Supabase session-pooler URI (see persistent storage below) |
Variables (non-sensitive tuning - visible in settings, safe to edit):
| Variable | Value |
|---|---|
| CCR_APP_URL | https://psychologicaltextanalysis.com |
| CCR_COOKIE_SECURE | 1 |
| CCR_MAX_ROWS | 20000 (global row ceiling; code default is 100000) |
| CCR_MAX_UPLOAD_BYTES | optional; code default is 52428800 (50 MB) |
| CCR_ANON_MAX_BYTES | optional; code default is 5242880 (5 MB) |
Paste CCR_APP_URL with no trailing space or newline. The app builds the
Google sign-in return URL from it, so a stray newline arrives at Supabase as
%0A inside redirect_to: the redirect stops matching the allow list and
browsers flag the link as dangerous. The app trims the value since 2026-08-17,
but older deployments and any other URL variable still take it literally.
CCR_MAX_ROWS is the limit that actually bounds a run - embedding cost scales
with rows and tokens, not file bytes, and on 2 vCPU it is time, not memory,
that runs out first. Measured on the cpu-basic Space shape (2 vCPU / 16 GB):
upload + parse peaks at roughly 5x file size, so even a 50 MB corpus costs
about 250 MB of the 16 GB available. The byte ceiling (CCR_MAX_UPLOAD_BYTES,
default 50 MB) is an abuse/OOM backstop that should not fire on a legitimate
corpus: at CCR_MAX_ROWS=20000 it only binds above ~2.6 KB per row, which is
already past every model's token window. Anonymous uploads use the lower of
that and CCR_ANON_MAX_BYTES (5 MB), sized as a pre-parse shield so an
unauthenticated request cannot make the server parse a large file only to
reject it at row 201.
Embedding throughput at 2 threads, batch 64 (see scripts/bench_models.py to
re-measure on the actual host - these are derated estimates, not Space-measured):
| model | ~15-word rows | ~60-word rows | ~250-word rows |
|---|---|---|---|
| MiniLM L6 v2 | ~1.2 s/1k | ~2.1 s/1k | ~8.4 s/1k |
| E5 Large v2 | ~7.5 s/1k | ~27 s/1k | ~123 s/1k |
At CCR_MAX_ROWS=20000 that is ~25 s to ~3 min for MiniLM, but up to ~40 min
for E5 Large on long documents. Jobs that long are also fragile: a Space
restart marks any running job failed (recover_orphaned_jobs), so worst-case
job duration - not row count alone - is the number to keep in view.
Retention (CCR_ANON_TTL_HOURS=24) and model pre-warm are already defaults in the Dockerfile.
Tester guide
The deployed instance serves a click-through testing guide at /guide with download links for every demo corpus (served from /samples). Send the PI and students that URL; no files need to be shared out of band.
Supabase setup for Google sign-in (one time)
- supabase.com > New project (free tier).
- Authentication > Providers > Google > Enable. Copy the shown callback URL (https://PROJECT_REF.supabase.co/auth/v1/callback).
- console.cloud.google.com > OAuth consent screen (External) > Credentials > Create OAuth client ID (Web application) > add the Supabase callback URL as an authorized redirect URI. Paste client id/secret back into the Supabase Google provider form.
- Authentication > URL Configuration: add BOTH redirect URLs:
- http://127.0.0.1:8000/api/auth/google/callback
- https://psychologicaltextanalysis.com/api/auth/google/callback Each entry must match what the app sends byte for byte, so keep the custom domain (not the hf.space host) here once the Worker is live.
- Project Settings > API: copy the Project URL and anon key into the Space secrets (and your local .env).
Push
git push origin main # GitHub
git push prod main # Hugging Face Space (rebuilds + redeploys)
The prod remote has no stored token; use the lab HF username and a WRITE token as the password when prompted (or a credential helper). Pushing to origin alone changes nothing on the deployed instance.
Caveats of the deployed instance
Ephemeral diskRESOLVED (2026-08-06, verified): the Space now runs withDATABASE_URL(Supabase Postgres) and theCCR_STORAGE=s3R2 secrets set, so accounts, constructs, runs, AND uploaded files survive rebuilds and restarts. The paragraph below ("Persistent storage") documents that setup for anyone redeploying from scratch. Without those secrets, the old caveat applies: SQLite + local files reset on every rebuild.- The lab Space runs on cpu-upgrade hardware, so it does not sleep. On the
free cpu-basic tier a Space sleeps after
48 h idle and the first visit wakes it (1 min), which is still the behaviour of the personal dev Space.
Persistent storage (make accounts/data survive restarts)
HF Spaces free disk is ephemeral - SQLite is wiped on every rebuild. Point the app at your Supabase Postgres (free, already used for Google auth):
- Supabase dashboard > Project Settings > Database > Connection string > "Session pooler". Copy the URI and put your DB password into it.
- Add it as a Space secret named
DATABASE_URL. - Restart the Space. First boot creates the tables in Postgres; data now persists across restarts and redeploys.
For durable uploaded FILES too (not just the database), also set the
CCR_STORAGE=s3 R2 secrets (see .env.example). Without that, the database
rows survive but a signed-in user's uploaded corpus file can still vanish on
restart (results CSVs are regenerable by re-running).