A short link is a row and a 302. Click analytics is the Linktree /go pattern with a country and referrer column. Dub is also open source, so the honest ceiling is self-hosting it; the DIY is for when you want the 200-line version on your own domain.
You are building a lean indie version of Dub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Dub · indie build A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | a row and a 302 | | Hosting | A VPS behind Caddy on a domain you will keep | a shared short link outlives the tool | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A short domain you will keep for years** · roughly $10 to $30 a year - Why: Every link you share depends on it. Auto-renew on. - Get it: A short .co, .link or .to at Porkbun or Cloudflare Registrar; turn on auto-renew. - [ ] **A random salt for hashing IPs** · free - Why: Click rows store hashes. - Get it: openssl rand -hex 32. - [ ] **An API token for scripts** · free - Why: Phase 4 exposes a token-protected API. - Get it: openssl rand -base64 32 into .env as API_TOKEN. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Conversion tracking, partner payouts, workspaces: the $90 product. - partner and affiliate payouts - conversion tracking through to revenue - team workspaces and folders - the polished analytics dashboard and API If one of those is essential to you, that is the reason to keep paying for Dub, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Dub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a link shortener with click analytics like Dub, for one person and one domain. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. No framework. One process. - Your own domain. A printed or shared short link outlives the tool, so this domain is forever. ### Data model (create this before Phase 1) - links: id, slug (unique, unambiguous alphabet), url, title, created_at, expires_at, active - clicks: id, link_id, clicked_at, referer_host, country, device_class, ip_hash Never store the raw IP or user agent; hash with a daily salt and bucket the device. ### Phase 1 · Redirects Build: GET /:slug issues a 302 to url; unknown or inactive slugs go to a configurable fallback page. Slugs are 6+ characters from an alphabet without 0/O/1/l/I, CSPRNG-generated, or custom when supplied. Reserved paths (admin, api, healthz) can never become slugs. Done when: a created slug redirects, an unknown one lands on the fallback, and creating a slug named admin is refused. Do not build yet: analytics, UI. ### Phase 2 · Click logging Build: after issuing the redirect, log the click (never before · a click must not wait on the database). Country from a cf-ipcountry style header when present. Bot user agents bucketed separately so link previews do not inflate counts. Done when: a click adds one row, a Slack preview fetch is bucketed as bot, and the redirect still works with the database stopped. ### Phase 3 · Admin and analytics Build: /admin behind basic auth from .env: create with optional custom slug and expiry, edit the target without changing the slug, deactivate; per link totals over 7 and 30 days, top referrers and countries, a clicks-per-day bar as inline SVG. Done when: editing a target keeps the slug, an expired link falls back, and totals reconcile with GROUP BY queries. ### Phase 4 · API and QR Build: a token-protected POST /api/links for scripts, and a QR image per link generated server-side. Done when: a curl with the token creates a link and the QR scans to it. ### Phase 5 · Deploy Build: /healthz, a nightly backup, a systemd unit, the README. Done when: state survives a reboot and the README goes from clone to a live short domain. ### Out of scope (and why) - Conversion tracking, partner payouts, workspaces. That is the $90 product. ### README must contain - The domain-is-forever warning. - The reserved-path list. ===== AGENTS.md ===== # Agent instructions · Dub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS behind Caddy on a domain you will keep. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · Dub A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Redirects GET /:slug 302s; unknown slugs fall back; reserved paths cannot be slugs. ### Steps 1. Create the project and tables links (id, slug unique, url, title, created_at, expires_at, active), clicks (id, link_id, clicked_at, referer_host, country, device_class, ip_hash). ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Slugs 6+ chars from an alphabet without 0/O/1/l/I via crypto.randomBytes, or custom; reserve admin, api, healthz ### Done when - [ ] A created slug redirects - [ ] An unknown one lands on FALLBACK_URL - [ ] Creating a slug named admin is refused ## Phase 2 · Click logging After the redirect, never before; bots bucketed. ### Steps 1. Insert the click after sending the 302; country from a cf-ipcountry style header when present 2. Bucket bot user agents separately ### Done when - [ ] A click adds one row - [ ] A Slack preview is bucketed as bot - [ ] The redirect works with the database stopped ## Phase 3 · Admin and analytics Create, edit target without changing the slug, deactivate, expire; stats per link. ### Steps 1. Basic-auth /admin with CRUD and expiry 2. Per-link 7 and 30 day totals, top referrers and countries, clicks-per-day inline SVG ### Done when - [ ] Editing a target keeps the slug - [ ] An expired link falls back - [ ] Totals reconcile with GROUP BY ## Phase 4 · API and QR A token-protected POST for scripts and a QR image per link. ### Steps 1. POST /api/links with Authorization: Bearer API_TOKEN 2. GET /admin/qr/:slug rendering a QR PNG server-side ```sh npm install qrcode@1 ``` ### Done when - [ ] A curl with the token creates a link - [ ] The QR scans to it ## Phase 5 · Deploy HTTPS, service, backup, README. ### Steps 1. /healthz, systemd, Caddy, nightly backup 2. README: domain-is-forever warning, reserved paths, the API Files: `README.md` ### Done when - [ ] State survives a reboot - [ ] Auto-renew confirmed on the registrar ## Not in this build - Conversion tracking, partner payouts, workspaces: the $90 product. ## After v1, if you want it - UTM parameters appended per link - Bulk import from a CSV ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/links.db # Required. The short domain. SITE_URL=https://yr.link # Required. Where unknown or inactive slugs land. FALLBACK_URL=https://yourdomain.com # Required · secret. openssl rand -hex 32. IP_SALT=hex # Required · secret. openssl rand -base64 32. API_TOKEN=base64 # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
You are building a lean indie version of Dub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Dub · indie build A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | a row and a 302 | | Hosting | A VPS behind Caddy on a domain you will keep | a shared short link outlives the tool | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A short domain you will keep for years** · roughly $10 to $30 a year - Why: Every link you share depends on it. Auto-renew on. - Get it: A short .co, .link or .to at Porkbun or Cloudflare Registrar; turn on auto-renew. - [ ] **A random salt for hashing IPs** · free - Why: Click rows store hashes. - Get it: openssl rand -hex 32. - [ ] **An API token for scripts** · free - Why: Phase 4 exposes a token-protected API. - Get it: openssl rand -base64 32 into .env as API_TOKEN. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Conversion tracking, partner payouts, workspaces: the $90 product. - partner and affiliate payouts - conversion tracking through to revenue - team workspaces and folders - the polished analytics dashboard and API If one of those is essential to you, that is the reason to keep paying for Dub, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Dub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a link shortener with click analytics like Dub, for one person and one domain. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. No framework. One process. - Your own domain. A printed or shared short link outlives the tool, so this domain is forever. ### Data model (create this before Phase 1) - links: id, slug (unique, unambiguous alphabet), url, title, created_at, expires_at, active - clicks: id, link_id, clicked_at, referer_host, country, device_class, ip_hash Never store the raw IP or user agent; hash with a daily salt and bucket the device. ### Phase 1 · Redirects Build: GET /:slug issues a 302 to url; unknown or inactive slugs go to a configurable fallback page. Slugs are 6+ characters from an alphabet without 0/O/1/l/I, CSPRNG-generated, or custom when supplied. Reserved paths (admin, api, healthz) can never become slugs. Done when: a created slug redirects, an unknown one lands on the fallback, and creating a slug named admin is refused. Do not build yet: analytics, UI. ### Phase 2 · Click logging Build: after issuing the redirect, log the click (never before · a click must not wait on the database). Country from a cf-ipcountry style header when present. Bot user agents bucketed separately so link previews do not inflate counts. Done when: a click adds one row, a Slack preview fetch is bucketed as bot, and the redirect still works with the database stopped. ### Phase 3 · Admin and analytics Build: /admin behind basic auth from .env: create with optional custom slug and expiry, edit the target without changing the slug, deactivate; per link totals over 7 and 30 days, top referrers and countries, a clicks-per-day bar as inline SVG. Done when: editing a target keeps the slug, an expired link falls back, and totals reconcile with GROUP BY queries. ### Phase 4 · API and QR Build: a token-protected POST /api/links for scripts, and a QR image per link generated server-side. Done when: a curl with the token creates a link and the QR scans to it. ### Phase 5 · Deploy Build: /healthz, a nightly backup, a systemd unit, the README. Done when: state survives a reboot and the README goes from clone to a live short domain. ### Out of scope (and why) - Conversion tracking, partner payouts, workspaces. That is the $90 product. ### README must contain - The domain-is-forever warning. - The reserved-path list. ===== AGENTS.md ===== # Agent instructions · Dub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS behind Caddy on a domain you will keep. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · Dub A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Redirects GET /:slug 302s; unknown slugs fall back; reserved paths cannot be slugs. ### Steps 1. Create the project and tables links (id, slug unique, url, title, created_at, expires_at, active), clicks (id, link_id, clicked_at, referer_host, country, device_class, ip_hash). ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Slugs 6+ chars from an alphabet without 0/O/1/l/I via crypto.randomBytes, or custom; reserve admin, api, healthz ### Done when - [ ] A created slug redirects - [ ] An unknown one lands on FALLBACK_URL - [ ] Creating a slug named admin is refused ## Phase 2 · Click logging After the redirect, never before; bots bucketed. ### Steps 1. Insert the click after sending the 302; country from a cf-ipcountry style header when present 2. Bucket bot user agents separately ### Done when - [ ] A click adds one row - [ ] A Slack preview is bucketed as bot - [ ] The redirect works with the database stopped ## Phase 3 · Admin and analytics Create, edit target without changing the slug, deactivate, expire; stats per link. ### Steps 1. Basic-auth /admin with CRUD and expiry 2. Per-link 7 and 30 day totals, top referrers and countries, clicks-per-day inline SVG ### Done when - [ ] Editing a target keeps the slug - [ ] An expired link falls back - [ ] Totals reconcile with GROUP BY ## Phase 4 · API and QR A token-protected POST for scripts and a QR image per link. ### Steps 1. POST /api/links with Authorization: Bearer API_TOKEN 2. GET /admin/qr/:slug rendering a QR PNG server-side ```sh npm install qrcode@1 ``` ### Done when - [ ] A curl with the token creates a link - [ ] The QR scans to it ## Phase 5 · Deploy HTTPS, service, backup, README. ### Steps 1. /healthz, systemd, Caddy, nightly backup 2. README: domain-is-forever warning, reserved paths, the API Files: `README.md` ### Done when - [ ] State survives a reboot - [ ] Auto-renew confirmed on the registrar ## Not in this build - Conversion tracking, partner payouts, workspaces: the $90 product. ## After v1, if you want it - UTM parameters appended per link - Bulk import from a CSV ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/links.db # Required. The short domain. SITE_URL=https://yr.link # Required. Where unknown or inactive slugs land. FALLBACK_URL=https://yourdomain.com # Required · secret. openssl rand -hex 32. IP_SALT=hex # Required · secret. openssl rand -base64 32. API_TOKEN=base64 # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
You are building a production product version of Dub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Dub · product brief ## Problem A short link is a row and a 302. Click analytics is the Linktree /go pattern with a country and referrer column. Dub is also open source, so the honest ceiling is self-hosting it; the DIY is for when you want the 200-line version on your own domain. ## Product outcome A shortener you could run for a team: token API, stats without personal data, and a domain treated as permanent infrastructure. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a domain you will keep forever - a small always-on host ## Explicit non-goals for v1 - Conversion tracking, partner payouts, workspaces: the $90 product. - partner and affiliate payouts - conversion tracking through to revenue - team workspaces and folders - the polished analytics dashboard and API ## Success criteria - Auto-renew on with a reminder - One restore drill performed - External slug check passing ===== BRIEF.md ===== # Build brief · Dub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a link shortener with click analytics like Dub, for one person and one domain. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. No framework. One process. - Your own domain. A printed or shared short link outlives the tool, so this domain is forever. ### Data model (create this before Phase 1) - links: id, slug (unique, unambiguous alphabet), url, title, created_at, expires_at, active - clicks: id, link_id, clicked_at, referer_host, country, device_class, ip_hash Never store the raw IP or user agent; hash with a daily salt and bucket the device. ### Phase 1 · Redirects Build: GET /:slug issues a 302 to url; unknown or inactive slugs go to a configurable fallback page. Slugs are 6+ characters from an alphabet without 0/O/1/l/I, CSPRNG-generated, or custom when supplied. Reserved paths (admin, api, healthz) can never become slugs. Done when: a created slug redirects, an unknown one lands on the fallback, and creating a slug named admin is refused. Do not build yet: analytics, UI. ### Phase 2 · Click logging Build: after issuing the redirect, log the click (never before · a click must not wait on the database). Country from a cf-ipcountry style header when present. Bot user agents bucketed separately so link previews do not inflate counts. Done when: a click adds one row, a Slack preview fetch is bucketed as bot, and the redirect still works with the database stopped. ### Phase 3 · Admin and analytics Build: /admin behind basic auth from .env: create with optional custom slug and expiry, edit the target without changing the slug, deactivate; per link totals over 7 and 30 days, top referrers and countries, a clicks-per-day bar as inline SVG. Done when: editing a target keeps the slug, an expired link falls back, and totals reconcile with GROUP BY queries. ### Phase 4 · API and QR Build: a token-protected POST /api/links for scripts, and a QR image per link generated server-side. Done when: a curl with the token creates a link and the QR scans to it. ### Phase 5 · Deploy Build: /healthz, a nightly backup, a systemd unit, the README. Done when: state survives a reboot and the README goes from clone to a live short domain. ### Out of scope (and why) - Conversion tracking, partner payouts, workspaces. That is the $90 product. ### README must contain - The domain-is-forever warning. - The reserved-path list. ===== ARCHITECTURE.md ===== # Architecture · Dub ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | a row and a 302 | | Hosting | A VPS behind Caddy on a domain you will keep | a shared short link outlives the tool | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Redirector | /:slug | A serverless function reading the same table | | Analytics | clicks and bucketing | Drop without touching redirects | | API | token routes | Add per-token scopes later | | Admin | CRUD and stats | Any UI; slugs are the contract | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `SITE_URL` · required · The short domain. - `FALLBACK_URL` · required · Where unknown or inactive slugs land. - `IP_SALT` · required, secret · openssl rand -hex 32. - `API_TOKEN` · required, secret · openssl rand -base64 32. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Dub product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS behind Caddy on a domain you will keep. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ===== MILESTONES.md ===== # Delivery milestones · Dub Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Redirects GET /:slug 302s; unknown slugs fall back; reserved paths cannot be slugs. ### Steps 1. Create the project and tables links (id, slug unique, url, title, created_at, expires_at, active), clicks (id, link_id, clicked_at, referer_host, country, device_class, ip_hash). ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Slugs 6+ chars from an alphabet without 0/O/1/l/I via crypto.randomBytes, or custom; reserve admin, api, healthz ### Done when - [ ] A created slug redirects - [ ] An unknown one lands on FALLBACK_URL - [ ] Creating a slug named admin is refused ## M2 · Click logging After the redirect, never before; bots bucketed. ### Steps 1. Insert the click after sending the 302; country from a cf-ipcountry style header when present 2. Bucket bot user agents separately ### Done when - [ ] A click adds one row - [ ] A Slack preview is bucketed as bot - [ ] The redirect works with the database stopped ## M3 · Admin and analytics Create, edit target without changing the slug, deactivate, expire; stats per link. ### Steps 1. Basic-auth /admin with CRUD and expiry 2. Per-link 7 and 30 day totals, top referrers and countries, clicks-per-day inline SVG ### Done when - [ ] Editing a target keeps the slug - [ ] An expired link falls back - [ ] Totals reconcile with GROUP BY ## M4 · API and QR A token-protected POST for scripts and a QR image per link. ### Steps 1. POST /api/links with Authorization: Bearer API_TOKEN 2. GET /admin/qr/:slug rendering a QR PNG server-side ```sh npm install qrcode@1 ``` ### Done when - [ ] A curl with the token creates a link - [ ] The QR scans to it ## M5 · Deploy HTTPS, service, backup, README. ### Steps 1. /healthz, systemd, Caddy, nightly backup 2. README: domain-is-forever warning, reserved paths, the API Files: `README.md` ### Done when - [ ] State survives a reboot - [ ] Auto-renew confirmed on the registrar ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the redirect is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443 ===== OPERATIONS.md ===== # Operations · Dub ## Backup SQLite .backup nightly; the links table is the asset. ## Restore Copy back; verify one slug. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring External check on a known slug. ## Incident checklist Domain expiry: renew in the grace period immediately. Leaked API token: rotate. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Auto-renew on with a reminder - [ ] One restore drill performed - [ ] External slug check passing ## Launch constraint Do not market omitted Dub capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/links.db # Required. The short domain. SITE_URL=https://yr.link # Required. Where unknown or inactive slugs land. FALLBACK_URL=https://yourdomain.com # Required · secret. openssl rand -hex 32. IP_SALT=hex # Required · secret. openssl rand -base64 32. API_TOKEN=base64 # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# Dub · indie build A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | a row and a 302 | | Hosting | A VPS behind Caddy on a domain you will keep | a shared short link outlives the tool | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A short domain you will keep for years** · roughly $10 to $30 a year - Why: Every link you share depends on it. Auto-renew on. - Get it: A short .co, .link or .to at Porkbun or Cloudflare Registrar; turn on auto-renew. - [ ] **A random salt for hashing IPs** · free - Why: Click rows store hashes. - Get it: openssl rand -hex 32. - [ ] **An API token for scripts** · free - Why: Phase 4 exposes a token-protected API. - Get it: openssl rand -base64 32 into .env as API_TOKEN. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Conversion tracking, partner payouts, workspaces: the $90 product. - partner and affiliate payouts - conversion tracking through to revenue - team workspaces and folders - the polished analytics dashboard and API If one of those is essential to you, that is the reason to keep paying for Dub, and the README should say so rather than pretend.
# Build brief · Dub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a link shortener with click analytics like Dub, for one person and one domain. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. No framework. One process. - Your own domain. A printed or shared short link outlives the tool, so this domain is forever. ### Data model (create this before Phase 1) - links: id, slug (unique, unambiguous alphabet), url, title, created_at, expires_at, active - clicks: id, link_id, clicked_at, referer_host, country, device_class, ip_hash Never store the raw IP or user agent; hash with a daily salt and bucket the device. ### Phase 1 · Redirects Build: GET /:slug issues a 302 to url; unknown or inactive slugs go to a configurable fallback page. Slugs are 6+ characters from an alphabet without 0/O/1/l/I, CSPRNG-generated, or custom when supplied. Reserved paths (admin, api, healthz) can never become slugs. Done when: a created slug redirects, an unknown one lands on the fallback, and creating a slug named admin is refused. Do not build yet: analytics, UI. ### Phase 2 · Click logging Build: after issuing the redirect, log the click (never before · a click must not wait on the database). Country from a cf-ipcountry style header when present. Bot user agents bucketed separately so link previews do not inflate counts. Done when: a click adds one row, a Slack preview fetch is bucketed as bot, and the redirect still works with the database stopped. ### Phase 3 · Admin and analytics Build: /admin behind basic auth from .env: create with optional custom slug and expiry, edit the target without changing the slug, deactivate; per link totals over 7 and 30 days, top referrers and countries, a clicks-per-day bar as inline SVG. Done when: editing a target keeps the slug, an expired link falls back, and totals reconcile with GROUP BY queries. ### Phase 4 · API and QR Build: a token-protected POST /api/links for scripts, and a QR image per link generated server-side. Done when: a curl with the token creates a link and the QR scans to it. ### Phase 5 · Deploy Build: /healthz, a nightly backup, a systemd unit, the README. Done when: state survives a reboot and the README goes from clone to a live short domain. ### Out of scope (and why) - Conversion tracking, partner payouts, workspaces. That is the $90 product. ### README must contain - The domain-is-forever warning. - The reserved-path list.
# Agent instructions · Dub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS behind Caddy on a domain you will keep. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Dub A link shortener on your own domain with click analytics: CSPRNG slugs, a redirect that never waits on the database, clicks with country, referrer and device but no raw IP, a private admin with per-link stats, a token API, and QR codes. Dub itself is open source; this is the 200-line version for one person and a domain you will keep forever. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Redirects GET /:slug 302s; unknown slugs fall back; reserved paths cannot be slugs. ### Steps 1. Create the project and tables links (id, slug unique, url, title, created_at, expires_at, active), clicks (id, link_id, clicked_at, referer_host, country, device_class, ip_hash). ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Slugs 6+ chars from an alphabet without 0/O/1/l/I via crypto.randomBytes, or custom; reserve admin, api, healthz ### Done when - [ ] A created slug redirects - [ ] An unknown one lands on FALLBACK_URL - [ ] Creating a slug named admin is refused ## Phase 2 · Click logging After the redirect, never before; bots bucketed. ### Steps 1. Insert the click after sending the 302; country from a cf-ipcountry style header when present 2. Bucket bot user agents separately ### Done when - [ ] A click adds one row - [ ] A Slack preview is bucketed as bot - [ ] The redirect works with the database stopped ## Phase 3 · Admin and analytics Create, edit target without changing the slug, deactivate, expire; stats per link. ### Steps 1. Basic-auth /admin with CRUD and expiry 2. Per-link 7 and 30 day totals, top referrers and countries, clicks-per-day inline SVG ### Done when - [ ] Editing a target keeps the slug - [ ] An expired link falls back - [ ] Totals reconcile with GROUP BY ## Phase 4 · API and QR A token-protected POST for scripts and a QR image per link. ### Steps 1. POST /api/links with Authorization: Bearer API_TOKEN 2. GET /admin/qr/:slug rendering a QR PNG server-side ```sh npm install qrcode@1 ``` ### Done when - [ ] A curl with the token creates a link - [ ] The QR scans to it ## Phase 5 · Deploy HTTPS, service, backup, README. ### Steps 1. /healthz, systemd, Caddy, nightly backup 2. README: domain-is-forever warning, reserved paths, the API Files: `README.md` ### Done when - [ ] State survives a reboot - [ ] Auto-renew confirmed on the registrar ## Not in this build - Conversion tracking, partner payouts, workspaces: the $90 product. ## After v1, if you want it - UTM parameters appended per link - Bulk import from a CSV
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/links.db # Required. The short domain. SITE_URL=https://yr.link # Required. Where unknown or inactive slugs land. FALLBACK_URL=https://yourdomain.com # Required · secret. openssl rand -hex 32. IP_SALT=hex # Required · secret. openssl rand -base64 32. API_TOKEN=base64 # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# Dub · product brief ## Problem A short link is a row and a 302. Click analytics is the Linktree /go pattern with a country and referrer column. Dub is also open source, so the honest ceiling is self-hosting it; the DIY is for when you want the 200-line version on your own domain. ## Product outcome A shortener you could run for a team: token API, stats without personal data, and a domain treated as permanent infrastructure. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a domain you will keep forever - a small always-on host ## Explicit non-goals for v1 - Conversion tracking, partner payouts, workspaces: the $90 product. - partner and affiliate payouts - conversion tracking through to revenue - team workspaces and folders - the polished analytics dashboard and API ## Success criteria - Auto-renew on with a reminder - One restore drill performed - External slug check passing
# Build brief · Dub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a link shortener with click analytics like Dub, for one person and one domain. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. No framework. One process. - Your own domain. A printed or shared short link outlives the tool, so this domain is forever. ### Data model (create this before Phase 1) - links: id, slug (unique, unambiguous alphabet), url, title, created_at, expires_at, active - clicks: id, link_id, clicked_at, referer_host, country, device_class, ip_hash Never store the raw IP or user agent; hash with a daily salt and bucket the device. ### Phase 1 · Redirects Build: GET /:slug issues a 302 to url; unknown or inactive slugs go to a configurable fallback page. Slugs are 6+ characters from an alphabet without 0/O/1/l/I, CSPRNG-generated, or custom when supplied. Reserved paths (admin, api, healthz) can never become slugs. Done when: a created slug redirects, an unknown one lands on the fallback, and creating a slug named admin is refused. Do not build yet: analytics, UI. ### Phase 2 · Click logging Build: after issuing the redirect, log the click (never before · a click must not wait on the database). Country from a cf-ipcountry style header when present. Bot user agents bucketed separately so link previews do not inflate counts. Done when: a click adds one row, a Slack preview fetch is bucketed as bot, and the redirect still works with the database stopped. ### Phase 3 · Admin and analytics Build: /admin behind basic auth from .env: create with optional custom slug and expiry, edit the target without changing the slug, deactivate; per link totals over 7 and 30 days, top referrers and countries, a clicks-per-day bar as inline SVG. Done when: editing a target keeps the slug, an expired link falls back, and totals reconcile with GROUP BY queries. ### Phase 4 · API and QR Build: a token-protected POST /api/links for scripts, and a QR image per link generated server-side. Done when: a curl with the token creates a link and the QR scans to it. ### Phase 5 · Deploy Build: /healthz, a nightly backup, a systemd unit, the README. Done when: state survives a reboot and the README goes from clone to a live short domain. ### Out of scope (and why) - Conversion tracking, partner payouts, workspaces. That is the $90 product. ### README must contain - The domain-is-forever warning. - The reserved-path list.
# Architecture · Dub ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | a row and a 302 | | Hosting | A VPS behind Caddy on a domain you will keep | a shared short link outlives the tool | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Redirector | /:slug | A serverless function reading the same table | | Analytics | clicks and bucketing | Drop without touching redirects | | API | token routes | Add per-token scopes later | | Admin | CRUD and stats | Any UI; slugs are the contract | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `SITE_URL` · required · The short domain. - `FALLBACK_URL` · required · Where unknown or inactive slugs land. - `IP_SALT` · required, secret · openssl rand -hex 32. - `API_TOKEN` · required, secret · openssl rand -base64 32. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Dub product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS behind Caddy on a domain you will keep. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Dub Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Redirects GET /:slug 302s; unknown slugs fall back; reserved paths cannot be slugs. ### Steps 1. Create the project and tables links (id, slug unique, url, title, created_at, expires_at, active), clicks (id, link_id, clicked_at, referer_host, country, device_class, ip_hash). ```sh mkdir shortener && cd shortener && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Slugs 6+ chars from an alphabet without 0/O/1/l/I via crypto.randomBytes, or custom; reserve admin, api, healthz ### Done when - [ ] A created slug redirects - [ ] An unknown one lands on FALLBACK_URL - [ ] Creating a slug named admin is refused ## M2 · Click logging After the redirect, never before; bots bucketed. ### Steps 1. Insert the click after sending the 302; country from a cf-ipcountry style header when present 2. Bucket bot user agents separately ### Done when - [ ] A click adds one row - [ ] A Slack preview is bucketed as bot - [ ] The redirect works with the database stopped ## M3 · Admin and analytics Create, edit target without changing the slug, deactivate, expire; stats per link. ### Steps 1. Basic-auth /admin with CRUD and expiry 2. Per-link 7 and 30 day totals, top referrers and countries, clicks-per-day inline SVG ### Done when - [ ] Editing a target keeps the slug - [ ] An expired link falls back - [ ] Totals reconcile with GROUP BY ## M4 · API and QR A token-protected POST for scripts and a QR image per link. ### Steps 1. POST /api/links with Authorization: Bearer API_TOKEN 2. GET /admin/qr/:slug rendering a QR PNG server-side ```sh npm install qrcode@1 ``` ### Done when - [ ] A curl with the token creates a link - [ ] The QR scans to it ## M5 · Deploy HTTPS, service, backup, README. ### Steps 1. /healthz, systemd, Caddy, nightly backup 2. README: domain-is-forever warning, reserved paths, the API Files: `README.md` ### Done when - [ ] State survives a reboot - [ ] Auto-renew confirmed on the registrar ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the redirect is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Dub ## Backup SQLite .backup nightly; the links table is the asset. ## Restore Copy back; verify one slug. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring External check on a known slug. ## Incident checklist Domain expiry: renew in the grace period immediately. Leaked API token: rotate. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Auto-renew on with a reminder - [ ] One restore drill performed - [ ] External slug check passing ## Launch constraint Do not market omitted Dub capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/links.db # Required. The short domain. SITE_URL=https://yr.link # Required. Where unknown or inactive slugs land. FALLBACK_URL=https://yourdomain.com # Required · secret. openssl rand -hex 32. IP_SALT=hex # Required · secret. openssl rand -base64 32. API_TOKEN=base64 # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
$ choose a build depth, inspect the files, then open the complete pack in your agent
Nobody pays $90 for short links. They pay for attributing signups and revenue to links and paying partners from that data, which is a finance product wearing a shortener.
xpartner and affiliate payouts
xconversion tracking through to revenue
xteam workspaces and folders
xthe polished analytics dashboard and API
Vibecode Dub
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Dub replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Dub cost?
Dub costs about $90/month (Business, checked 2026-09-04), which is $1080 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Dub?
Honestly: partner and affiliate payouts; conversion tracking through to revenue; team workspaces and folders; the polished analytics dashboard and API. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Dub?
Yes: Dub (the product itself is open source), Shlink (self-hosted URL shortener). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.