Host it yourself, in three commands.

Kytelink is open source, ground up. Nothing is closed, and every third-party service has a drop-in open-source or self-hostable equivalent. Run it on your own box, or use ours — either way it costs nothing.

The promise

Every hosted piece has an open swap-in.

The founder's hosted config is one example configuration, not a requirement. The app code never assumes a provider — it only speaks standard protocols: the Postgres wire protocol, Redis, the S3 API, SMTP, and OpenAI's HTTP shape.

Hosted stackSelf-hosted equivalent
Cloudflare R2MinIO, or any S3-compatible bucket
ResendSMTP (any provider, or your own Postfix)
ClickHouse CloudClickHouse OSS (a VM, or the compose service)
Neon (Postgres)Plain Postgres (compose, RDS, your own box)
OpenAIAny OpenAI-compatible endpoint, incl. local models
Vercel domain automationManual DNS
Fastest path

Three commands to a live stack.

One interactive wizard handles the whole first boot. Postgres and Redis are the only hard requirement — it runs them in Docker for you. Analytics, uploads, and a local email inbox are each one y/n question; skip any of them and that feature simply switches off gracefully.

$ git clone <this repo> && cd kytelink/v2
$ pnpm install
$ pnpm run setup
# asks what you want (Postgres is the only must), then writes
# .env with fresh secrets, starts Docker, migrates, and seeds
$ pnpm dev
→ web:3000 · landing:3001 · admin:3002 · api:3003 🪁

Change your mind later by re-running the wizard or editing .env — and if you run pnpm dev before setting up, it stops with the exact command to run instead of crashing.

Per-service setup

Wire up what you need.

PostgresRequired

The primary datastore: users, kytes, memberships, invites, assets, domains, moderation, schedules. Any Postgres 16+ works.

DATABASE_URLDIRECT_URL
RedisRequired

Caches, rate limits, BullMQ queues, and the beacon buffer. Any Redis 7+ (or protocol-compatible store).

REDIS_URL
ClickHouseOptional · analytics

Backs page, link, and product analytics and their rollups. Skip it entirely and the app runs with analytics off.

CLICKHOUSE_URLCLICKHOUSE_PASSWORD
S3 storageOptional · uploads

Owned static assets and user uploads share one bucket. Any S3 API works: MinIO locally, Cloudflare R2, or AWS S3.

AWS_ENDPOINT_URLAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_REGIONAWS_S3_BUCKETNEXT_PUBLIC_CDN_URL
EmailOptional · smtp default

Three backends via EMAIL_PROVIDER: console (stdout), smtp (mailpit locally, Postfix in prod), or resend.

EMAIL_PROVIDEREMAIL_FROMSMTP_HOSTSMTP_PORTRESEND_API_KEY
Moderation + AI importOptional · off

MODERATION_PROVIDER=none auto-approves everything. Set openai to enable moderation and the AI link importer.

MODERATION_PROVIDEROPENAI_API_KEYOPENAI_BASE_URLMODERATION_MODEL
Custom domainsOptional · proxy

DOMAIN_PROVIDER=proxy verifies by DNS and lets your reverse proxy issue certificates on demand — point CUSTOM_DOMAIN_A_RECORD/CNAME_TARGET at your edge and run the shipped Caddy config. On Vercel, set vercel with an API token instead.

DOMAIN_PROVIDERCUSTOM_DOMAIN_A_RECORDCUSTOM_DOMAIN_CNAME_TARGETVERCEL_TOKENVERCEL_TEAMVERCEL_PROJECT
Capability matrix

What turns off, gracefully.

A disabled capability always means the UI surface is absent or replaced with one calm card — never a broken button. Its tRPC procedures return a typed FEATURE_DISABLED error as the backstop.

MissingOffWhat the user sees
CLICKHOUSE_URLanalyticsBeacon endpoints return 202 and drop; no Analytics tab in the editor; admin shows a calm “Analytics is off” card. Everything else works.
AWS_* (S3)uploadsUpload tiles hidden; onboarding offers built-in default avatars; OG cards fall back to text-only. Owned static assets still serve.
RESEND_API_KEY / SMTPemailDeliveryOTPs and invites print to server stdout. Set EMAIL_PROVIDER=smtp against mailpit to read them in an inbox instead.
OPENAI_API_KEYmoderationPublishes auto-approve and the AI “Other” importer hides. Linktree/Beacons/Bio.link imports still work (deterministic parsers).
GOOGLE_* / GITHUB_*that OAuth buttonThe button is hidden from the auth screen; email OTP always works.
VERCEL_*domainsThe custom-domain UI shows DNS instructions instead of automated verification.
Production notes

Before you go live.

ISR cache volume

apps/web's Next.js ISR cache should live on a persistent volume in production, or page revalidation resets on every deploy.

Run workers separately

Set PROCESS_ROLE=worker on a dedicated process for the BullMQ workers — don't run them inline with the process serving HTTP traffic.

Back up Postgres

Postgres is the can't-lose store. Use pg_dump on a schedule, or point-in-time recovery if your provider offers it.

Back up your bucket

The S3 bucket holds irreplaceable user uploads. Periodic sync or cross-region replication is recommended. ClickHouse loss is accepted as lossy.

Read every line, then run it.

The full guide, docker-compose file, and env reference live in the repo. It's yours.

View on GitHub