Files
Primora/QUICK_START.md
T
Tomas DvorakandDevin c6166de886 feat: instance administration, bootstrap admin, and managed-tier OAuth gate
Self-hosted instances are now email/password only — OAuth providers
(GitHub/Google/Discord/Microsoft) activate solely when PRIMORA_MANAGED=true,
reserved for the future hosted tier.

Sign-up lifecycle: the first public sign-up becomes the instance admin; after
bootstrap, sign-up closes by default. A better-auth databaseHook gates only
/sign-up/email, so admin-created users and OAuth flows bypass it. The login
page reads GET /instance/public to hide the sign-up tab, show the bootstrap
notice, and render only the providers the instance offers.

Instance settings live in core.settings (migration 00010) and resolve
in-app > env > built-in default, cached 15s. Platform admins (better-auth
user.role) manage sign-up, mail transport, and rate limits via
GET/PUT/DELETE /instance/settings or the new Settings → Instance tab. Secret
values are AES-256-GCM encrypted at rest with PRIMORA_ENCRYPTION_KEY, never
returned by the API, and decrypted by the auth service at send time.

Auth service reads the same settings table for the sign-up gate and mail
config, so in-app changes take effect without a restart.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-21 17:46:47 +02:00

4.8 KiB

Primora — Quick Start

Prerequisites

  • Docker with the Compose plugin (recommended path), or
  • Go 1.26+, Node.js 20+, and a local PostgreSQL + Redis-compatible instance
./scripts/setup.sh

The script creates .env from .env.example, generates JWT_SECRET and BETTER_AUTH_SECRET, prompts for a domain (default localhost), and runs docker compose up -d.

Manual equivalent:

cp .env.example .env
# edit .env: set JWT_SECRET and BETTER_AUTH_SECRET
docker compose up -d --build

Open http://localhost. Sign up, create an organization, create a project.

Mailpit (local email capture) is at http://localhost/mailpit/.

Port conflicts

Only host bindings are configurable. If 5432/6379/80 are taken:

POSTGRES_PORT=5433
DRAGONFLY_PORT=6380
NGINX_PORT=8081

Internal service addresses (postgres:5432, dragonfly:6379) never change.

Demo mode (no backend required)

  • Append ?demo=true to the dashboard URL, or
  • set VITE_DEMO_MODE=true, or
  • click launch demo mode on the login page.

Demo mode is fully client-side: organizations, projects, members, buckets, objects, collections, API keys, and audit events all work and persist for the session. A "Demo — exit" pill in the topbar leaves demo mode.

Configuration

All configuration lives in .env (see .env.example for the annotated list). The important ones:

Variable Purpose
JWT_SECRET Signing secret for API tokens — required, generate with openssl rand -base64 32
BETTER_AUTH_SECRET Better Auth secret — required, same generation
POSTGRES_* / DATABASE_URL Postgres connection
DRAGONFLY_URL Cache/rate-limit store
SMTP_* / MAIL_FROM Transactional email (Mailpit in dev, or Resend via RESEND_API_KEY)
SIGNUP_ENABLED Open public sign-up after bootstrap — default false; admins override in-app
PRIMORA_MANAGED Managed tier flag — enables OAuth providers when true; self-hosted is email/password only
USER_RATE_LIMIT_PER_MINUTE / API_KEY_RATE_LIMIT_PER_MINUTE Throttling defaults — overridable in-app
VITE_DEMO_MODE Enable the client-side demo workspace

Instance administration

The first public sign-up becomes the instance admin — on a fresh database the login page only offers sign-up. After that, public sign-up is closed and the admin can reopen it in-app.

Admins get a Settings → Instance tab that manages the instance without a restart: public sign-up, mail transport (Resend or SMTP), and rate limits. Every setting resolves in the order in-app value → .env → built-in default; resetting a key reverts to env/default. Secrets (SMTP password, Resend key) are stored AES-256-GCM-encrypted and never displayed back.

API equivalents:

GET    /api/v1/instance/public           # unauthenticated — login page state
GET    /api/v1/instance/settings         # platform admin
PUT    /api/v1/instance/settings/{key}   # platform admin — {"value": ...}
DELETE /api/v1/instance/settings/{key}   # platform admin — revert to env/default

Verification

curl http://localhost/api/v1/health/liveness     # {"status":"ok"}
curl http://localhost/api/v1/health/readiness    # db + cache + storage checks
curl http://localhost/auth-health                # auth service + db + cache

Quality checks

npm run check            # everything: go test, tsc, builds, codegen drift
npm test                 # vitest across workspaces
cd apps/backend && go test ./...
cd apps/frontend && npx tsc -p tsconfig.json --noEmit

Regenerating code

Generated code is checked in but must never be hand-edited.

npm run generate:sqlc      # after changing db queries/migrations
npm run generate:client    # after changing apps/backend/openapi/openapi.yaml

npm run check fails CI if generated output drifts from the sources.

Troubleshooting

docker compose up fails on port binding — another service owns the port. Set POSTGRES_PORT/DRAGONFLY_PORT/NGINX_PORT in .env.

Login works but every API call 401s — the backend can't reach the auth service's JWKS. Confirm AUTH_INTERNAL_BASE_URL=http://auth:3001 and that the auth container is up (docker compose ps).

Emails never arrive — they go to Mailpit in development: http://localhost/mailpit/. For real delivery configure SMTP_* or RESEND_API_KEY.

Frontend dev server can't reach the API — Vite does not proxy. Either run the full compose stack and use http://localhost, or set VITE_API_BASE_URL=http://localhost/api/v1 and VITE_AUTH_BASE_URL=http://localhost/auth in apps/frontend/.env.local.

npm run check reports generated drift — run npm run generate:sqlc and/or npm run generate:client and commit the regenerated files.