Self-Hosting Guide
Contentrain Studio supports self-hosting of the AGPL core. This guide covers everything you need to deploy and operate Studio in your own environment; managed Pro/Enterprise operation may also be available separately.
Core Principles
- The application is stateless -- no local writable state beyond normal container runtime behavior
- Git repositories remain external and authoritative -- Studio never stores content in its database
- Auth/data are supplied through provider-backed integrations (swappable)
- Enterprise features degrade safely when
ee/functionality or external services are absent
Prerequisites
Required
| Component | Purpose |
|---|---|
| Public URL | A reachable HTTPS endpoint for the application |
| One auth + database provider pair | Either a Supabase-compatible deployment, or any plain PostgreSQL 14+ instance with the managed auth provider. See Choosing a Provider Pair. |
| GitHub App | Repository access, installation lifecycle, commit operations |
NUXT_SESSION_SECRET | Minimum 32 characters, used for AES-256 cookie encryption |
Optional
| Component | Purpose | When Needed |
|---|---|---|
| Anthropic API key | Operator-managed AI agent | If users should not need BYOA keys |
| Resend API key | Email notifications (invites, reminders); magic-link + invite auth emails on the managed pair | For team invite workflows; required on the managed + postgres pair |
| Polar or Stripe keys | Billing / subscription management (Polar is the default provider; Stripe is legacy) | For plan-gated deployments |
| Cloudflare R2 | Object storage for CDN and media | For CDN delivery and media library |
| Redis | Distributed rate limiting | For multi-instance deployments |
Choosing a Provider Pair
Studio's auth and database providers ship as matched pairs, selected by environment variables and enforced at boot. Two pairs are supported; mixing them (e.g. Supabase auth with a plain-Postgres database) is rejected at startup.
| Pair | Env | Auth | Database |
|---|---|---|---|
supabase + supabase (default) | NUXT_AUTH_PROVIDER=supabase, NUXT_DATABASE_PROVIDER=supabase | Supabase Auth (GitHub/Google OAuth, magic link) | Supabase PostgreSQL |
managed + postgres | NUXT_AUTH_PROVIDER=managed, NUXT_DATABASE_PROVIDER=postgres | Built-in JWT auth: HS256 access/refresh tokens, GitHub/Google OAuth run in-app, magic link via Resend | Any plain PostgreSQL 14+ |
The managed + postgres pair removes the Supabase dependency entirely -- bring any PostgreSQL (RDS, Railway, a container, your own cluster). Both pairs run the same RLS migration lineage, so row-level isolation is identical.
Supabase pair (default) requires
NUXT_SUPABASE_URL,NUXT_SUPABASE_SERVICE_ROLE_KEY,NUXT_SUPABASE_ANON_KEY
Managed + postgres pair requires
NUXT_POSTGRES_URL-- the connection stringNUXT_AUTH_JWT_SECRET-- minimum 32 characters, signs access/refresh tokensNUXT_RESEND_API_KEY-- magic-link + invite emails (required on this pair)NUXT_OAUTH_GITHUB_CLIENT_ID/NUXT_OAUTH_GITHUB_CLIENT_SECRET-- a GitHub OAuth App for login (callback{NUXT_PUBLIC_SITE_URL}/api/auth/oauth/github); Google OAuth is optionalNUXT_SESSION_PASSWORD-- minimum 32 characters, session store for the OAuth module (dev auto-generates it; deployed builds must set it)
INFO
The managed auth provider and the managed deployment profile are unrelated concepts that happen to share a name. Either provider pair can run under any deployment profile (Community or On-Premise EE).
See the Environment Variables reference for the full per-variable detail.
Deployment Shapes
Recommended options:
- Single container behind a reverse proxy (simplest)
- Railway / Render / Fly.io Docker deployment
- VM or bare-metal Docker deployment
- Kubernetes with one web deployment plus external backing services
Docker Deployment
Pull the Prebuilt Image (Recommended)
Official container images are published to GHCR on every v* release tag. For production, pull an exact tag rather than a floating one so upgrades are explicit:
docker pull ghcr.io/contentrain/studio:vX.Y.ZAvailable tags per release:
:vX.Y.Z(and:vX.Y.Z-prerelease.N) -- exact release pin (recommended for production):sha-<shortsha>-- immutable commit pin:X.Y,:X,:latest-- floating tags, stable releases only (prereleases never publish:latest)
Deploy a tagged release, not main HEAD, and avoid :latest in production.
Build the Image (Fork / Dev)
For forks, local iteration, or patched builds, build the image yourself:
docker build -t contentrain-studio .The Docker image contains:
- Built Nuxt/Nitro server output
- Generated
.contentrainassets for UI strings gitbinary for repository operations- Non-root
studiouser (security best practice)
Run the Container
docker run \
--name contentrain-studio \
--env-file .env \
-p 3000:3000 \
contentrain-studioThe container listens on port 3000.
Docker Compose Example
version: '3.8'
services:
studio:
image: contentrain-studio
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
# If using self-hosted Supabase:
# supabase:
# image: supabase/postgres:latest
# ...Health Check
The application exposes a health endpoint:
curl http://localhost:3000/api/health
# Returns 200 OKEditions and Deployment Profiles
Self-hosting behavior is driven by two orthogonal concepts:
- Edition -- whether the proprietary
ee/directory is present and loaded at runtime. Community Edition runs the AGPL core alone; Enterprise Edition addsee/under a separate commercial license. - Deployment profile -- a named preset (
managed/dedicated/on-premise/community) that configures billing mode and plan source coherently. SetNUXT_DEPLOYMENT_PROFILEexplicitly, or leave it unset to auto-detect at boot.
Self-hosting typically means one of two profiles:
| Profile | Edition | Billing | Plan source | Result |
|---|---|---|---|---|
community | AGPL only (ee/ absent) | Off | Fixed | Every workspace resolves to the fixed community tier with unlimited core usage; requires_ee features are force-disabled. Zero-license path. |
on-premise | AGPL core + licensed ee/ | Off | Operator-set | Full feature set on your own infra; operator sets each workspace's plan (default enterprise) via the DB. Requires an executed On-Premises Deployment License. |
Auto-detection when NUXT_DEPLOYMENT_PROFILE is unset: ee/ missing -> community; ee/ loaded + no billing env -> on-premise; ee/ loaded + Polar/Stripe configured -> managed.
Community Edition (community profile)
Use this for the AGPL core without the ee/ proprietary implementations. No agreement with Contentrain required.
Configure:
- One provider pair (Supabase, or plain PostgreSQL via the managed pair)
- GitHub App
NUXT_ANTHROPIC_API_KEY(operator-provided; the Studio-hosted key isee/-only)ee/directory absent; no Polar / Stripe env vars- Redis only if running multiple instances
This gives you:
- Authenticated app, workspace/project management, repository connection
- Full content operations (all 4 model kinds, all 27 field types)
- Branch / diff / merge and auto-merge workflows, forms (storage + captcha + notifications)
- AI chat with the operator-provided Anthropic key
- Every workspace at the fixed
communitytier with unlimited usage
Features that require ee/ (media/CDN, BYOA UI, Conversation API, outbound webhooks, reviewer/viewer roles, spam filter, SSO, white-label) are disabled in Community Edition. See Enterprise Edition for the full matrix.
On-Premise Enterprise (on-premise profile)
Use this with an executed On-Premises Deployment License to run the full feature set on your own infrastructure. Billing is off; the operator sets the workspace plan directly.
Additionally configure:
ee/directory present (matching the core release tag)NUXT_DEPLOYMENT_PROFILE=on-premise(optional -- auto-detected fromee/+ empty billing env)- Redis (multi-instance), Resend or internal SMTP, Cloudflare R2 (CDN/media)
- Set the plan per workspace, e.g.
UPDATE workspaces SET plan = 'enterprise' WHERE id = '...';
See Enterprise Edition for how editions map to plan tiers and which features require ee/.
Operational Surfaces (either profile)
Delivery and automation surfaces layer on top of either profile:
- Redis -- distributed rate limiting (multi-instance)
- Resend (or internal SMTP) -- email notifications
- Cloudflare R2 -- CDN and media delivery (
ee/feature) - Polar or Stripe -- only if you want managed billing on-prem (uncommon; promotes the profile to
dedicatedwith subscription-driven plans)
External Service Setup
Complete the setup steps for your chosen provider pair only.
Database + Auth -- Supabase pair (default)
- Create a Supabase project (or self-host Supabase)
- Apply all database migrations before first production traffic (see Migrations)
- Configure environment variables:
NUXT_SUPABASE_URL=https://your-project.supabase.co
NUXT_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
NUXT_SUPABASE_ANON_KEY=your-anon-key- Configure OAuth providers in Supabase Dashboard:
- GitHub OAuth: for workspace owners (needs repo access)
- Google OAuth: for invited team members
- Email/Magic Link: for passwordless auth
WARNING
The service role key has admin access to your database. Keep it private and never expose it to the client. Studio rejects an anon key pasted into the service-role slot at boot.
Database + Auth -- managed + postgres pair
Use this to run Supabase-free on any plain PostgreSQL 14+ instance.
- Provision a PostgreSQL 14+ database (RDS, Railway, a container, your own cluster)
- Create a GitHub OAuth App for login (callback
{NUXT_PUBLIC_SITE_URL}/api/auth/oauth/github); optionally a Google OAuth client - Apply migrations against the database (see Migrations)
- Configure environment variables:
NUXT_AUTH_PROVIDER=managed
NUXT_DATABASE_PROVIDER=postgres
NUXT_POSTGRES_URL=postgres://user:password@host:5432/contentrain
NUXT_AUTH_JWT_SECRET=your-random-32-character-string
NUXT_SESSION_PASSWORD=your-random-32-character-string
NUXT_OAUTH_GITHUB_CLIENT_ID=your-github-oauth-app-client-id
NUXT_OAUTH_GITHUB_CLIENT_SECRET=your-github-oauth-app-client-secret
# Google OAuth is optional (sign-in button hidden when unset):
# NUXT_OAUTH_GOOGLE_CLIENT_ID=...
# NUXT_OAUTH_GOOGLE_CLIENT_SECRET=...
NUXT_RESEND_API_KEY=re_your-resend-api-key # required: magic-link + invite emailsWARNING
NUXT_RESEND_API_KEY is required on the managed pair -- the managed auth provider sends magic-link and invite emails through Resend. Rotating NUXT_AUTH_JWT_SECRET invalidates all outstanding tokens (users re-login); NUXT_SESSION_PASSWORD stores no durable state and can be rotated freely.
Migrations
Both pairs run the same RLS migration lineage; apply migrations before first production traffic.
Supabase pair -- uses the Supabase CLI:
pnpm db:migrate # supabase migration up (reads supabase/migrations/*)Managed + postgres pair -- uses the bundled plain-PG runner:
pnpm db:migrate:pg # applies the lineage (reads NUXT_POSTGRES_URL, or pass --url)
pnpm db:verify:pg # verifies schema, trigger chain, and RLS isolationBoth plain-PG commands are idempotent -- re-runs skip already-applied files via public.schema_migrations.
GitHub App
- Create a GitHub App at
https://github.com/settings/apps/new - Configure permissions:
- Repository contents: Read & Write
- Pull requests: Read & Write
- Metadata: Read-only
- Set the callback URL to
{SITE_URL}/auth/callback - Set the webhook URL to
{SITE_URL}/api/webhooks/github - Generate a private key (
.pemfile) - Base64-encode the private key:
base64 -i your-app.private-key.pem | tr -d '\n'- Configure environment variables:
NUXT_GITHUB_APP_ID=your-app-id
NUXT_GITHUB_CLIENT_ID=your-client-id
NUXT_GITHUB_CLIENT_SECRET=your-client-secret
NUXT_GITHUB_PRIVATE_KEY=base64-encoded-pem-content
NUXT_GITHUB_WEBHOOK_SECRET=your-hmac-webhook-secret
NUXT_PUBLIC_GITHUB_APP_SLUG=your-app-slugEmail (Resend)
NUXT_RESEND_API_KEY=re_your-resend-api-key
NUXT_EMAIL_SENDER_ADDRESS=[email protected]
NUXT_EMAIL_SENDER_NAME=Contentrain StudioIf using local Supabase auth with SMTP, also set:
RESEND_API_KEY=re_your-resend-api-keyBilling (Polar / Stripe)
Billing is off by default and is uncommon for self-hosts. Polar is the default payment provider; Stripe is supported as a legacy plugin. When both are configured, Polar wins. Configuring a payment plugin requires the licensed ee/ bridge.
Polar (default):
NUXT_POLAR_ACCESS_TOKEN=polar_oat_your-organization-access-token
NUXT_POLAR_WEBHOOK_SECRET=your-polar-webhook-signing-secret
NUXT_POLAR_STARTER_PRODUCT_ID=uuid-of-starter-product
NUXT_POLAR_PRO_PRODUCT_ID=uuid-of-pro-product
NUXT_POLAR_SERVER=productionStripe (legacy / optional):
NUXT_STRIPE_SECRET_KEY=sk_live_your-stripe-secret-key
NUXT_STRIPE_WEBHOOK_SECRET=whsec_your-webhook-signing-secret
NUXT_STRIPE_STARTER_PRICE_ID=price_starter-monthly-id
NUXT_STRIPE_PRO_PRICE_ID=price_pro-monthly-idNUXT_PUBLIC_BILLING_ENABLED is auto-derived at boot from the configured payment plugins; set it explicitly only to override.
When no payment plugin is configured (the usual self-host case), Studio runs in no-billing mode and the subscription UI is hidden. The resulting plan depends on the deployment profile:
communityprofile (ee/absent) -- every workspace resolves to the fixedcommunitytier: unlimited core usage, with allrequires_eefeatures force-disabled.on-premiseprofile (ee/loaded, no billing) -- the operator sets each workspace's plan directly (defaultenterprise).
CDN / Object Storage (Cloudflare R2)
NUXT_CDN_R2_ACCOUNT_ID=your-cloudflare-account-id
NUXT_CDN_R2_ACCESS_KEY_ID=your-r2-access-key-id
NUXT_CDN_R2_SECRET_ACCESS_KEY=your-r2-secret-access-key
NUXT_CDN_R2_BUCKET=contentrain-cdnRedis
For multi-instance production deployments:
REDIS_URL=redis://localhost:6379
# Use REDIS_CA_CERT for custom CA trust with rediss://Without Redis, rate limiting degrades to in-memory and is only suitable for single-instance setups.
Production Checklist
Base
- [ ] Set
NODE_ENV=production - [ ] Set
NUXT_PUBLIC_SITE_URLto your public application URL - [ ] Set a strong
NUXT_SESSION_SECRET(minimum 32 characters) - [ ] Run behind TLS (reverse proxy or platform load balancer)
Database / Auth
- [ ] Choose a provider pair via
NUXT_AUTH_PROVIDER/NUXT_DATABASE_PROVIDER - [ ] Supabase pair: provide all three Supabase variables (
URL,SERVICE_ROLE_KEY,ANON_KEY) - [ ] Managed pair: provide
NUXT_POSTGRES_URL,NUXT_AUTH_JWT_SECRET,NUXT_SESSION_PASSWORD,NUXT_OAUTH_GITHUB_*, andNUXT_RESEND_API_KEY - [ ] Apply all database migrations before first production traffic (
pnpm db:migratefor Supabase,pnpm db:migrate:pg+pnpm db:verify:pgfor the managed pair) - [ ] Configure OAuth callback URLs correctly
GitHub
- [ ] Create and configure GitHub App
- [ ] Provide all five GitHub variables
- [ ] Test webhook delivery to
/api/webhooks/github
Reverse Proxy
Your reverse proxy should:
- Forward HTTPS traffic to port
3000 - Preserve
Hostand standard forwarding headers (X-Forwarded-For,X-Forwarded-Proto) - Set a sane request body limit for media uploads (50MB+)
- Not cache authenticated application routes
- Support Server-Sent Events (SSE) for chat streaming
Secret Rotation
For session/encryption secret rotation:
- Set the new secret in
NUXT_SESSION_SECRET - Keep the old value in
NUXT_SESSION_SECRET_PREVIOUS - Deploy the new version
- Allow existing sessions to refresh (token auto-refresh handles this)
- Remove
NUXT_SESSION_SECRET_PREVIOUSafter the migration window
Backup Strategy
Studio itself does not require local writable state. Back up these external components:
| Component | Strategy |
|---|---|
| Database | Supabase automated backups or pg_dump |
| Object Storage | R2 bucket versioning / cross-region replication |
| Git Repositories | Already distributed (GitHub) |
| Deployment Secrets | Secure vault (AWS Secrets Manager, Vault, etc.) |
Do not treat the Studio container filesystem as durable state.
Post-Deploy Smoke Checks
After first deploy, verify:
/api/healthreturns200- Login page loads
- OAuth callback URLs are correct
- A workspace can be created and listed
- GitHub App installation flow completes
- A repository can be scanned and connected
- At least one chat / content flow works end-to-end
- Email, CDN, media, and billing surfaces work (if configured)
Release Validation
Before cutting a release image:
pnpm release:checkThis runs lint, typecheck, and build verification.
Related Pages
- Environment Variables -- complete variable reference
- Architecture -- system design and provider pattern
- Enterprise Edition -- what requires
ee/