Self Hosting LightCMS
Step by step guide to self hosting LightCMS, covering the GitHub App, PostgreSQL, environment variables, optional AI, image CDN and email services, and deploying to Vercel.
Want LightCMS fully under your control? LightCMS is a SvelteKit application so it can be deployed anywhere, backed by a postgre database and a GitHub App(octokit). Your content never leaves your own GitHub repositories.
This guide walks you through everything you need to run your own instance: creating the GitHub App, provisioning PostgreSQL, configuring environment variables, and deploying.
How the Pieces Fit Together
| Component | Purpose | Required |
|---|---|---|
| PostgreSQL | Stores users, sessions, passkeys and GitHub installations | Yes |
| GitHub App | Reads and writes content in your repositories, and signs users in with GitHub | Yes |
| SMTP provider | Sends magic-link sign-in emails | Yes, for magic-link sign-in |
| OpenAI-compatible API | Powers the AI Copilot | Optional |
| Speech-to-text API | Powers voice dictation | Optional |
| Image CDN | Hosts images uploaded in the editor | Optional |
| PostHog | Product analytics and AI observability | Required in development only |
Prerequisites
- mise: installs and managing tasks dependencies. I will be assuming you have it installed.
- PostgreSQL: a database that accepts TLS connections. Cloud providers such as Neon, Supabase or Nile works well. See awesome-free-postgres for free options.
- GitHub account: needed to register the GitHub App.
- SMTP provider (for magic links): If you have a domain then Resend will be having an awesome experience, if not then Zoho Mail remember to create a app password.
TLS is mandatory. The application connects to PostgreSQL with
ssl: "require". A local PostgreSQL instance without TLS enabled will be rejected, so use a hosted database or configure TLS on your own server.
1. Fork the repository and install dependencies
Clone the repository and install dependencies:
cd lightcms
mise trust
mise deps
mise run install
Install the toolchain and the project dependencies:
mise trust
mise deps
mise run install
2. Create the GitHub App
LightCMS uses a GitHub App for two things: signing users in with GitHub (OAuth), and acting on your behalf through installations. You can create it with the built in Probot setup wizard, or by hand.
Option A: Probot setup wizard
start the bot with:
mise run bot
With no credentials in .env, Probot starts a setup server at http://localhost:3000:
- Open
http://localhost:3000and click Register GitHub App. - Choose your personal account or an organization.
- Confirm. Probot creates the app and writes its credentials to your
.envfile.
The wizard sets the app’s webhook URL to a Smee channel (or http://localhost:3000/), not to LightCMS’s webhook
endpoint. Update it in the GitHub App settings before going live (see Production deployment).
Option B: Manual setup
Create the app in GitHub Settings → Developer settings → GitHub Apps → New GitHub App with these values:
| Setting | Value |
|---|---|
| Homepage URL | https://<your-domain> |
| Callback URL | https://<your-domain>/api/auth/callback/github |
| Setup URL | https://<your-domain>/github/setup |
| Webhook URL | https://<your-domain>/api/github/webhook |
| Webhook secret | A random secret (see Generating secrets) |
Repository permissions
| Permission | Access | Used for |
|---|---|---|
Contents |
Read and write | Reading and committing content files |
Issues |
Read and write | Review comments and labels on pull requests |
Pull requests |
Read-only | Listing the files changed in a pull request |
Commit statuses |
Read and write | Reporting content validation on pushes |
Metadata |
Read-only | Mandatory for every GitHub App |
Subscribe to events: Push and Pull request. Installation events are delivered to GitHub Apps automatically.
After creating the app, note the App ID, generate a client secret, and generate and download a private key
(.pem).
3. Configure environment variables
Create a .env file in the project root. The repository does not ship a usable example file, so use the reference below.
Required
# ---------------------------------------------------------------------------
# Authentication (Better Auth)
# ---------------------------------------------------------------------------
BETTER_AUTH_SECRET="<output of: openssl rand -hex 32>"
# Public URL of this instance. Must match the URL users visit.
BETTER_AUTH_URL="http://localhost:5173"
# ---------------------------------------------------------------------------
# Database (PostgreSQL, TLS required)
# ---------------------------------------------------------------------------
DATABASE_URL="postgresql://user:password@host:5432/lightcms?sslmode=require"
# ---------------------------------------------------------------------------
# GitHub App
# ---------------------------------------------------------------------------
GITHUB_APP_ID="123456"
GITHUB_CLIENT_ID="Iv1.xxxxxxxxxxxx"
GITHUB_CLIENT_SECRET="<github-app-client-secret>"
GITHUB_WEBHOOK_SECRET="<webhook-secret>"
GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----"
GITHUB_PRIVATE_KEY is the full PEM contents. Keep the line breaks and wrap the value in double quotes, which is how
the Probot wizard writes it.
Required for development
LightCMS reports analytics and errors to PostHog. When the keys are missing, the app throws in development to stop events being silently dropped. In production they are optional, and telemetry is skipped when they are unset.
PUBLIC_POSTHOG_KEY="phc_xxxxxxxxxxxx"
PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
Email: required for magic-link sign-in
Magic links are sent over SMTP with Nodemailer. Sending fails with a configuration error when
MAIL_HOST, MAIL_USER, MAIL_PASS or MAIL_FROM is missing.
MAIL_HOST="smtp.resend.com"
# Defaults to 587 when omitted
MAIL_PORT="465"
# "true", "1", "yes" or "on" enables implicit TLS. When omitted, TLS is enabled only for port 465.
MAIL_SECURE="1"
MAIL_USER="resend"
MAIL_PASS="<smtp-password-or-api-key>"
MAIL_FROM="LightCMS <[email protected]>"
Optional: AI Copilot
The Copilot uses any OpenAI-compatible chat completions API. All three variables are needed for it to work.
OPEN_AI_ENDPOINT="https://api.openai.com/v1"
OPEN_AI_APIKEY="sk-..."
OPEN_AI_MODEL="gpt-4o-mini"
Any provider that exposes an OpenAI-compatible API works, for example OpenAI,
OpenRouter (https://openrouter.ai/api/v1) or NVIDIA NIM.
Optional: speech-to-text dictation
LightCMS supports two transcription backends. When both are configured, the OpenAI-compatible backend is used.
OpenAI-compatible transcription. It is selected when SPEECH_API_KEY is set, and also needs SPEECH_MODEL:
SPEECH_API_KEY="<api-key>"
SPEECH_MODEL="whisper-1"
# Base URL of the provider. Omit it to use the OpenAI default.
SPEECH_ENDPOINT="https://api.openai.com/v1"
Point SPEECH_ENDPOINT at any provider that implements the /audio/transcriptions API, and set SPEECH_MODEL to a
model that provider offers. A trailing /audio/transcriptions in the endpoint is stripped automatically.
Wit.ai is used when SPEECH_API_KEY is not set:
WIT_AI_TOKEN="<wit-ai-server-access-token>"
If neither is configured, dictation is unavailable.
Optional: image uploads (CDN)
Images pasted or uploaded in the editor are sent to a Chevereto-compatible upload API. Hosted options include FreeImage.host and ImgBB, or you can run your own Chevereto instance.
CDN_URL="https://freeimage.host/api/1/upload"
CDN_KEY="<cdn-api-key>"
CDN_KEY is sent as the key query parameter unless CDN_URL already contains one. If the upload fails or the
variables are unset, LightCMS falls back to embedding the image in the document as a base64 data: URL. This works,
but it makes your Markdown files much larger, so configure a CDN for real use.
Local webhook forwarding (development only)
WEBHOOK_PROXY_URL="https://smee.io/<your-channel>"
This is read by Probot, not by the SvelteKit app. See Testing webhooks locally.
Generating secrets
Use any of these to generate BETTER_AUTH_SECRET and GITHUB_WEBHOOK_SECRET:
openssl rand -hex 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
4. Set up the database
LightCMS uses Drizzle ORM. The schema lives in src/lib/server/db/schema.ts, and migrations
are generated into the drizzle directory. With DATABASE_URL set, apply the migrations:
bun run db:migrate
During local development you can instead push the schema straight to the database, without migration files:
bun run db:push
Other database scripts:
| Command | Description |
|---|---|
bun run db:generate |
Generate a migration after editing the schema |
bun run db:studio |
Browse and edit your data in Drizzle Studio |
5. Run locally
bun run dev


LightCMS is now available at http://localhost:5173. Set BETTER_AUTH_URL to this address, and sign in with GitHub for the first time.
Testing webhooks locally
GitHub cannot deliver webhooks to localhost, so forward them through smee.io:
-
Create a channel at smee.io and copy its URL.
-
Set
WEBHOOK_PROXY_URLin.envto that URL. -
Set the Webhook URL in your GitHub App settings to the same URL.
-
Run the bot in a second terminal:
bun run bot:start
The bot registers the same handlers as the production endpoint: content validation on push, review comments and
labels on pull_request, and installation logging.
6. Production deployment
The project is configured with sveltekit which is very vendor agnostic, so Vercel netlify cloudflare workers all are supported deployment targets out of the box.
- Import your Git repository into deployment platform.
- Add every environment variable from step 3 in Project Settings → Environment
Variables. Set
BETTER_AUTH_URLto your production URL. - Deploy.
To verify the production build locally:
bun run build
bun run preview
Troubleshooting
Make a account with Wit.ai it’s a free service by meta | And any openai compatible API for transcription. | One can use groq or assemblyai or | elevenlabs or deepgram or sarvam. |
For development purpose you can use the following credentials for posthog
PUBLIC_POSTHOG_KEY = “phc_AS2JXQsqEwNNreH5kra5Eefd5TijExE5eiLfwdzXLhPg” PUBLIC_POSTHOG_URL = ”https://us.i.posthog.com”
PUBLIC_POSTHOG_KEY=phc_AS2JXQsqEwNNreH5kra5Eefd5TijExE5eiLfwdzXLhPg
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

WIT_AI_TOKEN="Server access token"
WIT_AI_CLIENT_TOKEN="Client access token"
APP_ID=
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
CDN_KEY=
CDN_URL=
DATABASE_URL=
GITHUB_APP_ID=
GITHUB_APP_NAME=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_PRIVATE_KEY=
GITHUB_WEBHOOK_SECRET=
MAIL_FROM=
MAIL_HOST=
MAIL_PASS=
MAIL_PORT=
MAIL_SECURE=
MAIL_USER=
OPEN_AI_ENDPOINT=
OPEN_AI_MODEL=
ORIGIN=
PRIVATE_KEY=
PUBLIC_POSTHOG_HOST=
PUBLIC_POSTHOG_KEY=
SPEECH_API_KEY=
SPEECH_ENDPOINT=
SPEECH_MODEL=
WEBHOOK_SECRET=
WIT_AI_CLIENT_TOKEN=
WIT_AI_TOKEN=
