Self-Hosting

Layout is fully open source and designed to run on your own infrastructure. This guide covers everything you need to deploy it, from environment variables to Docker builds to platform-specific notes.

Prerequisites

Before deploying, make sure you have the following:

Node.js 20+
Required for local development. Docker builds handle this automatically.
PostgreSQL database
Used by Better Auth for user accounts. Any PostgreSQL 14+ instance works: Supabase, Neon, Railway, or self-hosted.
Supabase instance
Used for project data storage (extractions and layout.md content). Can be self-hosted or Supabase Cloud.
Anthropic API key
Required for layout.md generation and the test panel. Get one at console.anthropic.com.
Figma Personal Access Token
Optional. Only needed if you want to extract design systems from Figma files.

Clone & Configure

Clone the repository and create your local environment file:

bash
git clone https://github.com/uselayout/app.git
cd app
cp .env.example .env.local

Open .env.local and fill in the values below. Variables marked as required must be set before the app will start.

VariableRequiredDescription
ANTHROPIC_API_KEYYesAnthropic API key for Claude, used for layout.md generation and the test panel.
BETTER_AUTH_SECRETYesAuth secret used to sign sessions. Generate with: openssl rand -base64 32
DATABASE_URLYesPostgreSQL connection string for Better Auth user tables.
NEXT_PUBLIC_SUPABASE_URLYesSupabase project URL, e.g. https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEYYesSupabase anonymous (public) key for client-side project data access.
NEXT_PUBLIC_APP_URLNoFull URL of your deployment. Defaults to http://localhost:3000 if unset.
BETTER_AUTH_URLNoShould match NEXT_PUBLIC_APP_URL. Used by Better Auth for redirect URLs.
FIGMA_DEFAULT_TOKENNoFigma Personal Access Token for extraction. Users can also provide their own in-app.
STRIPE_SECRET_KEYNoOnly needed if you want to enable the billing / Pro tier features.
STRIPE_WEBHOOK_SECRETNoStripe webhook signing secret. Only needed for billing.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYNoStripe publishable key for client-side checkout. Only needed for billing.

Database Setup

Better Auth (PostgreSQL)

Better Auth manages user accounts and sessions. It creates its own tables on first run, with no migration command needed. The four tables it creates are:

layout_user

layout_session

layout_account

layout_verification

Your DATABASE_URL must point to a PostgreSQL database that the app has permission to create tables in. A fresh database is fine, and no manual schema setup is required.

Port 5432 must be accessible from your app server. If you are using a self-hosted PostgreSQL instance, check your firewall rules to ensure the port is open for inbound connections from the app.

Supabase (project data)

Supabase stores extraction results and layout.md content per project. You need a projects table. Run the migration from supabase/migrations/ in the repo, or apply it manually via the Supabase SQL editor.

If you are running a self-hosted Supabase instance, do not add the ssl option to your connection config. Self-hosted Supabase does not use SSL by default. The standard connection string without SSL options is correct.

Docker Deployment

The recommended way to deploy is via Docker. The included Dockerfile is a multi-stage build: deps installs node modules, builder runs next build, and runner produces the final slim image with Playwright Chromium pre-installed.

NEXT_PUBLIC_* variables are baked into the client JavaScript bundle at build time. They must be passed as --build-arg values during docker build, not set as runtime environment variables. All other variables (API keys, secrets) are runtime only and should be passed via --env-file.
bash
docker build \
  --build-arg NEXT_PUBLIC_SUPABASE_URL=https://your-supabase.example.com \
  --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \
  --build-arg NEXT_PUBLIC_APP_URL=https://studio.yourdomain.com \
  -t layout-studio .

docker run -d \
  --env-file .env.local \
  -p 3000:3000 \
  layout-studio

The container exposes port 3000. Put Nginx, Caddy, or a load balancer in front of it to handle TLS termination.

Platform Notes

Coolify

Connect your GitHub repo and set the branch to main. Coolify auto-detects the Dockerfile. In the Environment Variables section, add the NEXT_PUBLIC_* vars and enable the Build Arg toggle for each one. All other vars can be added as regular runtime environment variables.

Railway

Connect the GitHub repo. Railway detects the Dockerfile automatically. Add all environment variables in the Variables tab. The NEXT_PUBLIC_* vars must be set before the first deploy so they are available during the build stage.

VPS (docker-compose)

For a self-managed VPS, use a docker-compose.yml that passes the build args from your .env.local file:

yaml
services:
  studio:
    build:
      context: .
      args:
        NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
        NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
        NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
    env_file: .env.local
    ports:
 - "3000:3000"
    restart: unless-stopped

Run docker-compose up -d --build from the repo root. Docker Compose reads .env.local automatically for variable substitution in the args block.

Playwright Setup

Playwright is required for website extraction only. Figma extraction, layout.md generation, and the test panel all work without it.

Playwright cannot run in Vercel serverless functions. If you want website extraction, you must use a Docker/VPS deployment. Figma extraction works on Vercel without any changes.

The Dockerfile already handles Playwright installation in the runner stage:

dockerfile
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers
RUN npx playwright install --with-deps chromium

If you are running the app without Docker (e.g. for local development or a bare Node.js deployment), install Chromium manually and set the environment variable:

bash
npx playwright install --with-deps chromium
export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers
The --with-deps flag installs the required OS-level dependencies (libnss3, libatk, etc.) alongside Chromium. This is needed on headless Linux servers. On macOS for local development, you can omit it.

Stripe (Optional)

Stripe integration is entirely optional. If you are running a personal or internal instance with no billing requirement, leave all STRIPE_* environment variables unset. The app starts and runs normally without them.

If you want to enable the billing / Pro tier:

  1. Create your products and prices in the Stripe Dashboard.
  2. Set STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in your environment.
  3. Create a webhook endpoint in the Stripe Dashboard pointing to https://studio.yourdomain.com/api/webhooks/stripe, then set STRIPE_WEBHOOK_SECRET to the signing secret.
For local Stripe webhook testing, use the Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe. This forwards live events to your local instance without needing a public URL.

Verify Your Deployment

Once deployed, run through these checks to confirm everything is working:

1

Marketing page loads

Visit the root URL. You should see the marketing homepage, not an error or blank screen.

2

Auth is working

Visit /api/auth. Better Auth should return a JSON response. A 404 means the auth route is not registered.

3

Website extraction works

Sign in, click "Extract now", enter a URL, and start extraction. Progress events should stream in.

4

layout.md generates

After extraction completes, click Generate. The editor panel should stream in the layout.md content.

5

Test panel responds

Open the test panel, type a prompt, and send it. You should get a streamed response from Claude.

If website extraction fails with a timeout or browser error, Playwright is likely not installed correctly. Check the container logs for PLAYWRIGHT_BROWSERS_PATH errors and confirm that npx playwright install --with-deps chromium ran successfully during the build.