Multi-tenant URL shortener SaaS built with Next.js, Fastify, PostgreSQL, Prisma, Redis, and a dedicated worker process.
It is designed as a real product foundation rather than a tutorial toy:
- branded short links
- workspaces and team access
- invitations and audit logs
- custom domains with verification and diagnostics
- async click processing and analytics
- exports, QR codes, API keys, abuse protection, monitoring hooks, and deployment-ready structure
apps/api: Fastify API and redirect serverapps/web: Next.js App Router frontendapps/worker: background job workerpackages/db: Prisma schema, client, migrations, queue helpers, object storage helperspackages/shared: shared constants, job kinds, API-key scopes, and payload typespackages/validation: shared validation package placeholdertests: runtime and browser testsscripts: runtime validation harnesses
package.json: workspace scriptspnpm-workspace.yaml: workspace definitionturbo.json: Turborepo pipelineplaywright.config.ts: browser E2E configuration.env.examples: environment referenceREADME.md: project guidedocs/SRS.md: detailed software requirements specification
- User registration, login, refresh, logout, password reset, email verification
- Workspace creation and multi-member collaboration
- Invitation creation, preview, accept, and revoke
- Link CRUD with slug generation, validation, tags, QR generation, export, and analytics
- Redirect resolution with Redis-backed caching and abuse protection
- Async worker for emails, click processing, and exports
- Custom domain onboarding, verification, diagnostics, enable/disable, and assignment
- API keys with scopes and per-key usage analytics
- Audit logs, ops overview, email delivery events, and abuse signals
- Sentry-ready monitoring and alert-webhook hooks
apps/webserves the user-facing frontendapps/apiserves the REST API and redirect endpointsapps/workerprocesses queued jobs from the database- PostgreSQL stores core transactional data
- Redis accelerates redirect lookups and can fall back safely when unavailable
- Object storage stores exported CSV files
- User interacts with the Next.js dashboard.
- The frontend calls Fastify REST endpoints.
- Fastify reads and writes PostgreSQL through Prisma.
- Redirects use Redis cache first, then database fallback.
- Heavy work is queued into the
jobstable. - The worker claims and processes jobs asynchronously.
app.ts: registers plugins, routes, cache verification, mail readiness, and error handlingserver.ts: boots the API serviceconfig/env.ts: env validationplugins/auth.ts: JWT auth, API-key auth, request usage loggingjwt.ts: Fastify JWT setupprisma.ts: Prisma injection
common/utils/abuse-guard.ts: in-memory rate and block trackingabuse-monitor.ts: persistence for abuse signalsalerts.ts: outbound operational alert webhookapi-key-scopes.ts: API-key route scope guardcustom-domains.ts: custom-domain helperslink-cache.ts: Redis + in-memory redirect cachemailer.ts: SMTP or Resend email sendermonitoring.ts: Sentry integrationreserved-slugs.ts: reserved path protectionworkspace-roles.ts: public/internal workspace role normalization
modules/auth: auth flowsworkspaces: workspace CRUD and membership-aware accesslinks: link CRUD and validationredirects: redirect resolutionanalytics: workspace and link analyticstags: tag CRUD and tag assignmentqr: QR generationexports: export job creation, listing, and downloadusers: current user profile and workspace membersapi-keys: API key creation, revoke, usage summaryaudit: workspace audit logsinvitations: invite workflowsdomains: custom-domain lifecycleemail: Resend webhook ingestionops: workspace operations overviewhealth: health and readiness endpoints
page.tsx: marketing/landing pagelogin,register,forgot-password,reset-password,verify-email,resend-verification,accept-invitation: auth and onboarding pagesdashboard/layout.tsx: authenticated dashboard shelldashboard/page.tsx: workspace summary and ops pulsedashboard/links/page.tsx: create links, create tags, search/manage links, export, QRdashboard/links/[linkId]/page.tsx: link detail and analyticsdashboard/links/[linkId]/edit/page.tsx: edit linkdashboard/analytics/page.tsx: analytics overviewdashboard/settings/page.tsx: profile, workspace, members, custom domains, security/ops guidancedashboard/members/page.tsx: invitation/member surfacedashboard/api-keys/page.tsx: scoped API-key management and usage visibilitydashboard/audit-logs/page.tsx: audit historydashboard/exports/page.tsx: export history and downloadsdashboard/change-password/page.tsx: password change
worker.ts: polling loop, health server, graceful shutdown, stale-lock recoveryprocessors/click-events.ts: analytics click aggregationprocessors/email-jobs.ts: queued email delivery + event persistenceprocessors/export-jobs.ts: CSV generation and object storage uploadutils/mailer.ts: SMTP or Resend senderutils/click-metadata.ts: bot/device/browser/referrer enrichmentutils/monitoring.ts: Sentry integrationutils/alerts.ts: operational alert webhook
client.ts: Prisma client exportjob-queue.ts: enqueue, claim, complete, fail jobsobject-storage.ts: local or Cloudflare R2 object storage abstractionindex.ts: package export surface
schema.prisma: complete data modelmigrations/: schema historyseed.ts: demo user/workspace/link seed
Core entities:
userssessionsworkspacesworkspace_memberslinkstagslink_tagslink_daily_statslink_click_eventslink_unique_visitorslink_daily_unique_visitorsworkspace_domainsqr_codesexport_jobsjobsapi_keysapi_key_request_eventsaudit_logsinvitationspassword_reset_tokensemail_verification_tokensemail_delivery_eventsabuse_signals
See docs/SRS.md for the full table-by-table specification.
GET /healthGET /readyGET /:slugGET /.well-known/url-shortener-domain-verification
POST /auth/registerPOST /auth/loginPOST /auth/refreshPOST /auth/logoutGET /auth/mePOST /auth/forgot-passwordPOST /auth/reset-passwordPOST /auth/resend-verificationPOST /auth/verify-emailPOST /auth/change-password
- workspaces
- links
- analytics
- tags
- QR
- exports
- users/members
- invitations
- custom domains
- API keys
- audit logs
- ops overview
The full route breakdown is documented in docs/SRS.md.
See .env.examples for the canonical list.
Most important:
DATABASE_URL=
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
APP_URL=
API_URL=
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_SHORT_URL_BASE=
REDIS_URL=
OBJECT_STORAGE_PROVIDER=local
EXPORT_STORAGE_DIR=.data/exports
EMAIL_PROVIDER=smtp
MAIL_FROM=
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
ALERT_WEBHOOK_URL=Recommended production choice: Cloudflare R2.
Why:
- S3-compatible integration
- simple switch from local storage
- low-cost practical option for early-stage SaaS
- no egress-charge pressure compared with more traditional object-storage setups
R2 envs:
OBJECT_STORAGE_PROVIDER=r2
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
R2_ENDPOINT=
R2_PUBLIC_BASE_URL=Supported providers:
smtpresend
Recommended production provider:
resend
Webhook endpoint:
POST /webhooks/resend
Built-in operational surfaces:
- API
/health - API
/ready - Worker
/health - Worker
/ready - Workspace
ops/overview
Monitoring hooks:
- Sentry-ready for API
- Sentry-ready for worker
- Sentry-ready for frontend
- outbound alert webhook for critical incidents
API keys are now scoped, not all-powerful.
Supported scopes:
links:readlinks:writeanalytics:readtags:readtags:writeexports:readexports:write
Machine-enabled routes accept:
X-API-Key: usk_xxxxxxxxxxxxxUser/admin dashboard routes still require JWT auth.
npm run testCurrent runtime validation covers:
- Redis outage fallback and reconnect
- stale job reclaim
- custom domain verification + redirect + analytics path
- export storage behavior
pnpm test:e2eSet:
E2E_BASE_URL=
E2E_USER_EMAIL=
E2E_USER_PASSWORD=Install:
pnpm installGenerate Prisma client if needed:
pnpm db:generateApply migrations:
pnpm db:deployRun services:
pnpm dev:web
pnpm dev:api
pnpm dev:worker- Provision managed PostgreSQL.
- Provision Redis.
- Set real JWT secrets.
- Set
APP_URL,API_URL, and frontend public URLs. - Put public traffic behind a CDN/WAF such as Cloudflare.
- Switch object storage to R2 for production.
- Configure email provider and webhook.
- Configure Sentry DSNs.
- Configure alert webhook.
- Enable backups and point-in-time recovery for Postgres and export storage.
- Deploy API and worker as separate services.
- Apply migrations.
- Validate health and readiness endpoints.
- Test custom-domain verification on real DNS and HTTPS.
- docs index: docs/DOCS_INDEX.md
- product structure: docs/Product_structure.md
- practical API guide: docs/api.md
- practical database guide: docs/database.md
- practical deployment guide: docs/deployment.md
- security guide: docs/security.md
- testing guide: docs/testing.md
- short roadmap: docs/roadmap.md
- architecture guide: docs/architecture.md
- contributing guide: docs/contributing.md
- changelog: docs/changelog.md
- Main system specification: docs/SRS.md
- API reference: docs/API_REFERENCE.md
- OpenAPI-style spec: docs/openapi.yaml
- Postman collection: docs/POSTMAN_COLLECTION.json
- DB schema reference: docs/DB_SCHEMA_REFERENCE.md
- Product PRD and roadmap: docs/PRODUCT_PRD_ROADMAP.md
- deployment and incident runbook: docs/DEPLOYMENT_RUNBOOK.md