Slite answers the obvious follow-up to Sink.
Sink is elegant if you already live on Cloudflare, but it is not a normal Docker app. Slite is the sibling version for people who want the same family of short-link features on their own server.
No Cloudflare D1. No KV namespace. No Analytics Engine. No R2 bucket. Just one Node.js process, a Docker volume, SQLite, DuckDB, and local files.
What is Slite?
Slite is a self-hosted URL shortener with analytics.
I inspected:
repo: https://github.com/miantiao-me/Slite
commit: a6af2ca6336d803e79fa55edab9644321629da78
tag: v0.3.1
license: AGPL-3.0-only
The upstream README says it plainly: Slite and Sink are sibling versions of the same link-management and analytics project. Sink runs on Cloudflare’s serverless platform; Slite runs locally as a Node.js 24+/Docker process with SQLite and DuckDB.
What You Get
Slite has the features you would expect from this project family:
- short links;
- custom slugs;
- optional case-sensitive matching;
- link expirations;
- password-protected links;
- unsafe-link warning pages;
- UTM handling;
- device routing;
- country routing;
- social preview metadata;
- QR codes;
- JSON import/export;
- CSV analytics export;
- OpenAPI docs;
- REST API;
- MCP endpoint at
/api/mcp; - optional AI slug and OpenGraph suggestions.
It is a small-team/personal shortener, not a multi-tenant SaaS attribution platform like Dub.
Stack
The current Slite stack is:
- Nuxt 4 for the dashboard.
- Nitro node-server for API routes, redirects, OpenAPI docs, and MCP.
- Node.js 24+ because Slite uses Node’s built-in
node:sqlite. - SQLite at
/data/slite.sqliteas the authoritative link store. - Drizzle ORM for schema and database access.
- DuckDB at
/data/analytics.duckdbfor click analytics. - unstorage memory driver for a rebuildable in-process link cache.
- unstorage filesystem driver for uploaded social images and JSON backups.
- DB-IP City Lite bundled in release images for GeoIP analytics.
- xsai for optional OpenAI-compatible AI providers.
- Docker Compose for the normal self-hosting path.
That makes Slite much closer to YOURLS, Kutt, Shlink, or Snapp from an operational point of view: you can run it on a VPS or homelab machine.
How It Relates to Sink
This is the useful mental model:
| Concern | Sink | Slite |
|---|---|---|
| Runtime | Cloudflare Workers | Local Node.js 24 process |
| Link database | Cloudflare D1 | SQLite |
| Redirect cache | Cloudflare KV | In-memory cache |
| Analytics | Cloudflare Analytics Engine | DuckDB |
| Files/backups | Cloudflare R2 | Local filesystem |
| AI | Cloudflare Workers AI | OpenAI-compatible provider via xsai |
| Deployment | Cloudflare bindings | Docker Compose |
So yes, Slite is related to Sink. It is not a random fork. It is the local self-hosted sibling.
Self-Hosting Slite with Docker
The reusable Home-Lab compose file is here:
The site includes the same Compose file from the local snippets folder:
assets/snippets/slite/docker-compose.yml
Pre-Requisites - Docker
Install Docker on your system before proceeding:
- Linux: Official Docker Engine install guide
- Windows / Mac: Docker Desktop
Verify installation: docker --version && docker compose version
services:
slite:
image: ghcr.io/miantiao-me/slite:v0.3.1
container_name: slite
restart: unless-stopped
init: true
ports:
- "${SLITE_PORT:-5483}:5483"
environment:
NUXT_SITE_TOKEN: ${NUXT_SITE_TOKEN:?Set NUXT_SITE_TOKEN in .env}
NUXT_DATA_DIR: /data
NUXT_TRUST_PROXY: ${NUXT_TRUST_PROXY:-false}
NUXT_CLIENT_IP_HEADER: ${NUXT_CLIENT_IP_HEADER:-}
NUXT_GEOIP_PATH: ${NUXT_GEOIP_PATH:-}
NUXT_PUBLIC_HOME_URL: ${NUXT_PUBLIC_HOME_URL:-}
NUXT_NOT_FOUND_REDIRECT: ${NUXT_NOT_FOUND_REDIRECT:-}
NUXT_REDIRECT_STATUS_CODE: ${NUXT_REDIRECT_STATUS_CODE:-301}
NUXT_REDIRECT_WITH_QUERY: ${NUXT_REDIRECT_WITH_QUERY:-false}
NUXT_REDIRECT_NO_STORE: ${NUXT_REDIRECT_NO_STORE:-false}
NUXT_CASE_SENSITIVE: ${NUXT_CASE_SENSITIVE:-false}
NUXT_DISABLE_BOT_ACCESS_LOG: ${NUXT_DISABLE_BOT_ACCESS_LOG:-false}
NUXT_DISABLE_AUTO_BACKUP: ${NUXT_DISABLE_AUTO_BACKUP:-false}
NUXT_SAFE_BROWSING_DOH: ${NUXT_SAFE_BROWSING_DOH:-}
NUXT_WEBHOOK_URL: ${NUXT_WEBHOOK_URL:-}
NUXT_WEBHOOK_SECRET: ${NUXT_WEBHOOK_SECRET:-}
NUXT_PUBLIC_LINK_PROXY_ENABLED: ${NUXT_PUBLIC_LINK_PROXY_ENABLED:-false}
NUXT_AI_BASE_URL: ${NUXT_AI_BASE_URL:-}
NUXT_AI_MODEL: ${NUXT_AI_MODEL:-}
NUXT_AI_API_KEY: ${NUXT_AI_API_KEY:-}
volumes:
- slite-data:/data
stop_grace_period: 30s
volumes:
slite-data:
Prepare the environment:
cd assets/snippets/slite
cp .env.sample .env
Set a private token:
NUXT_SITE_TOKEN=replace-with-a-long-private-token
Then start Slite:
docker compose up -d
The default local URL is:
http://localhost:5483/dashboard
Use NUXT_SITE_TOKEN to sign in.
Why the Token Is Required in the Snippet
Upstream allows NUXT_SITE_TOKEN to be empty.
That keeps public redirects working, but Slite generates a random in-memory token that is not logged and not persisted. In practice, that means the dashboard and API are not usable after startup.
For a reusable self-hosting snippet, making NUXT_SITE_TOKEN required is better. If you run:
docker compose up -d
without setting it, Compose stops immediately and tells you what is missing.
Storage and Backups
Everything important lives under /data.
| Path | Purpose |
|---|---|
/data/slite.sqlite |
Authoritative links and app state |
/data/analytics.duckdb |
Click analytics |
/data/files/images |
Uploaded social preview images |
/data/backups |
Link JSON backups |
Automatic link backups run every 24 hours and keep the latest 30 automatic snapshots.
Those JSON backups are useful for portability, but they are not full system backups. They do not include DuckDB analytics history or uploaded images.
For a complete backup, stop the container and copy the entire /data volume.
Operating Constraints
Slite is a single-process app.
Do not run multiple replicas against the same /data volume. SQLite, DuckDB, and the in-memory cache are local-process tools, not a distributed storage layer.
That is not a flaw; it is the tradeoff that makes Slite simple. One container, one persistent data directory, one short-link app.
Reverse Proxy Notes
For public use, put Slite behind HTTPS with Caddy, Nginx, Traefik, or Cloudflare Tunnel.
Keep:
NUXT_TRUST_PROXY=false
unless Slite is reachable only through a proxy that overwrites forwarded headers.
If you want Slite to trust a specific proxy-provided client IP header, use:
NUXT_CLIENT_IP_HEADER=CF-Connecting-IP
Only do that when the app is not directly reachable from the public internet, otherwise clients can spoof headers.
Optional AI
AI is disabled by default.
To enable AI-assisted slugs or OpenGraph metadata, set:
NUXT_AI_BASE_URL=https://api.openai.com/v1
NUXT_AI_MODEL=gpt-4o-mini
NUXT_AI_API_KEY=...
Local or self-hosted OpenAI-compatible providers can work too, and the API key can stay empty when the provider does not require one.
One important privacy distinction versus Sink: Slite’s docs say the AI routes send only the destination URL you provide. Slite does not scrape the destination page and send page content to the model.
Field Test
I smoke-tested the Home-Lab compose with the official image:
SLITE_PORT=8791 \
NUXT_SITE_TOKEN=temporary-test-token \
docker compose -f /home/jalcocert/Desktop/Home-Lab/slite/docker-compose.yml \
-p slite_smoke up -d
Observed:
- image pulled:
ghcr.io/miantiao-me/slite:v0.3.1; - container started and listened on internal port
5483; - GeoIP loaded from
/app/geoip/dbip-city-lite.mmdb; GET /returned200;GET /dashboardreturned307to/dashboard/links;- authenticated
GET /api/verifyreturned200; - authenticated
POST /api/link/createcreated aslite-smokelink; GET /slite-smokereturned301tohttps://example.com/slite-smoke.
The test stack was stopped after the smoke test.
FAQ
Is Slite related to Sink?
Yes.
Slite and Sink are sibling versions of the same link-management project. Sink is the Cloudflare-native version. Slite is the local Docker/Node.js version.
Is Slite basically Sink without Cloudflare D1 and KV?
Yes, that is the right mental model.
Slite keeps the Sink-style shortener and API direction, but swaps the Cloudflare services for local components:
| Sink piece | Slite replacement |
|---|---|
| Cloudflare Workers | Node.js 24 / Nitro container |
| Cloudflare D1 | SQLite at /data/slite.sqlite |
| Cloudflare KV | In-memory process cache |
| Cloudflare Analytics Engine | DuckDB at /data/analytics.duckdb |
| Cloudflare R2 | Local filesystem under /data/files/images and /data/backups |
| Cloudflare Workers AI | Optional OpenAI-compatible provider via xsai |
The tradeoff is that Slite is a single-node app. Run one Slite process per /data directory.
Does Slite require Cloudflare?
No.
Slite does not need Cloudflare Workers, D1, KV, Analytics Engine, or R2. It uses local SQLite, DuckDB, memory cache, and filesystem storage.
Does Slite need Postgres, MySQL, or Redis?
No.
Slite is intentionally simpler: SQLite stores links, DuckDB stores analytics, and the cache lives in process memory.
Can I run Slite on a VPS?
Yes.
That is the natural deployment target. Run one Docker container, keep /data persistent, and put a TLS reverse proxy in front of it.
Can I scale Slite horizontally?
No, not safely.
Run one Slite process per data directory. Do not point multiple containers or clustered Node workers at the same SQLite/DuckDB files.
Does Slite have analytics?
Yes.
Visit events are stored in local DuckDB. The dashboard uses polling and client-side replay for a near-real-time feel; it does not need WebSockets or SSE.
Does Slite have an API?
Yes.
Every instance serves:
/_docs/openapi.json
/_docs/scalar
/_docs/swagger
API requests use:
Authorization: Bearer YOUR_SITE_TOKEN
Should I choose Slite or Sink?
Choose Slite if you want a normal Docker deployment on your own server.
Choose Sink if you want the Cloudflare-native deployment model and are comfortable using D1, KV, Analytics Engine, and related bindings.
Comments