Skip to content

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:

  1. Open http://localhost:3000 and click Register GitHub App.
  2. Choose your personal account or an organization.
  3. Confirm. Probot creates the app and writes its credentials to your .env file.

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"

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

imagebot

permissions detailed

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:

  1. Create a channel at smee.io and copy its URL.

  2. Set WEBHOOK_PROXY_URL in .env to that URL.

  3. Set the Webhook URL in your GitHub App settings to the same URL.

  4. 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.

  1. Import your Git repository into deployment platform.
  2. Add every environment variable from step 3 in Project Settings → Environment Variables. Set BETTER_AUTH_URL to your production URL.
  3. 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

https://wit.ai

wit.ai

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=

configuration for the bot