A fast, feature-rich paste-sharing platform for developers. Share code snippets, text notes, CLI logs, and Markdown documents with syntax highlighting, end-to-end encryption, workspace collaboration, and more.
Monorepo powered by pnpm workspaces + Turborepo.
apps/
api/ → Express 5 REST API (controller → service → repository)
web/ → Next.js 16 frontend (App Router, SSR)
worker/ → BullMQ background job processor
cli/ → Command-line interface (Commander 14)
packages/
config/ → Shared tsconfig, eslint, prettier, env loader
crypto/ → Password hashing (scrypt), SHA-256, ID generation, AES-256-GCM E2E encryption
db/ → Prisma 7 schema, client, migrations, seed
sdk/ → Typed API client for frontend, CLI & scripts
storage/ → Hybrid storage: DB (≤64 KB) + MinIO/S3 (>64 KB)
types/ → Shared domain enums and DTO types
validation/ → Zod 4 schemas for paste, auth, moderation, workspaces, secret scanning
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, Tailwind CSS 4, Plate.js (rich editor) |
| Backend | Express 5, Pino, Zod 4, Stripe 22 |
| Worker | BullMQ 5, ioredis |
| Database | PostgreSQL 16 via Prisma 7 |
| Cache / Queues | Redis 7 |
| Object Storage | MinIO (local) / S3-compatible (production) |
| Auth | Email + password, GitHub OAuth, Google OAuth, session cookies, API tokens |
| Encryption | AES-256-GCM client-side via Web Crypto API |
| Language | TypeScript 6 |
| Tooling | pnpm 9, Turborepo, Vitest 4, Prettier |
- Node.js ≥ 22
- pnpm ≥ 9
- Docker & Docker Compose
# 1. Clone and install
git clone <repo-url> pasteking
cd pasteking
pnpm install
# 2. Start infrastructure (PostgreSQL, Redis, MinIO)
docker compose up -d
# 3. Configure environment
cp .env.example .env
# Edit .env — at minimum set SESSION_SECRET to a random ≥32-char string
# 4. Set up database
pnpm db:generate
pnpm db:migrate
# 5. Seed sample data (optional — creates demo users, pastes, workspaces)
pnpm db:seed
# 6. Start the full stack
pnpm devServices start at:
| Service | URL |
|---|---|
| Web | http://localhost:3000 |
| API | http://localhost:4000 |
| MinIO | http://localhost:9001 |
| Worker | (background process) |
| Password | Role | |
|---|---|---|
| admin@pasteking.dev | admin123 | ADMIN |
| demo@pasteking.dev | demo1234 | USER |
| alice@example.com | alice1234 | USER |
| bob@example.com | bobsecure | USER |
| carol@example.com | carol5678 | RESTRICTED |
| Command | Description |
|---|---|
pnpm dev |
Start all apps in dev mode |
pnpm build |
Build all apps and packages |
pnpm lint |
Lint all packages |
pnpm typecheck |
Type-check all packages |
pnpm test |
Run all tests (Vitest) |
pnpm format |
Format code with Prettier |
pnpm db:generate |
Generate Prisma client |
pnpm db:migrate |
Run database migrations |
pnpm db:seed |
Seed database with sample data |
pnpm db:studio |
Open Prisma Studio |
pnpm clean |
Remove dist/, node_modules/, .next |
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /v1/health |
API v1 health check |
| POST | /v1/auth/register |
Register (email + username + password) |
| POST | /v1/auth/login |
Login (returns session cookie) |
| GET | /v1/auth/oauth/:provider |
Start OAuth flow (github, google) |
| GET | /v1/auth/oauth/:provider/callback |
OAuth callback (exchanges code for session) |
| POST | /v1/pastes |
Create a paste (anonymous or authenticated) |
| POST | /v1/pastes/raw |
Create paste from raw text body |
Query params: ?mode=, ?visibility=, ?expiresIn=, ?burnAfterRead=true, ?title=, ?language= |
||
| GET | /v1/pastes/search |
Full-text search public pastes (?q=, ?limit=, ?offset=) |
| GET | /v1/pastes/:id |
Get paste with content (records view for analytics) |
| GET | /v1/pastes/:id/raw |
Get raw paste content |
| PATCH | /v1/pastes/:id |
Update paste (anonymous or owner) |
| DELETE | /v1/pastes/:id |
Delete paste (owner or via x-delete-token) |
| GET | /v1/pastes/:id/revisions |
Revision history |
| GET | /v1/pastes/:id/revisions/:from/diff/:to |
Diff between two revisions |
| POST | /v1/pastes/:id/fork |
Fork a paste (copies content into a new paste) |
| Method | Path | Description |
|---|---|---|
| POST | /v1/auth/logout |
Logout (clears session) |
| GET | /v1/auth/me |
Get current user |
| POST | /v1/auth/tokens |
Create API token (with optional scopes) |
| GET | /v1/auth/tokens |
List API tokens |
| DELETE | /v1/auth/tokens/:id |
Revoke API token |
| GET | /v1/pastes/mine |
List authenticated user's pastes |
| GET | /v1/pastes/search/mine |
Search user's own pastes (?q=, ?limit=, ?offset=, ?language=, ?mode=) |
| GET | /v1/pastes/analytics/me |
User analytics (total pastes/views/forks) |
| GET | /v1/pastes/:id/analytics |
Paste analytics (owner only) |
| POST | /v1/reports |
Report a paste |
| Path | Description |
|---|---|
/v1/ws?pasteId=... |
Real-time collaboration (edit, cursor, presence) |
| Method | Path | Description |
|---|---|---|
| GET | /v1/admin/pastes/search |
Search/filter all pastes (?q=, ?status=, ?moderationStatus=, ?visibility=, ?authorId=) |
| GET | /v1/admin/reports |
List reports |
| GET | /v1/admin/reports/:id |
Get report details |
| PATCH | /v1/admin/reports/:id/status |
Update report status |
| GET | /v1/admin/pastes/:id/moderation |
Get paste moderation info |
| POST | /v1/admin/pastes/:id/actions |
Take moderation action |
| GET | /v1/admin/users |
List users |
| GET | /v1/admin/users/:id |
Get user details |
| PATCH | /v1/admin/users/:id/status |
Update user status |
| GET | /v1/admin/flags |
List abuse flags |
| GET | /v1/admin/audit-logs |
List audit logs |
| Scope | Description |
|---|---|
paste:create |
Create new pastes |
paste:read:own |
Read own pastes |
paste:delete:own |
Delete own pastes |
Empty scopes = full access (backwards compatible).
PasteKing supports two auth methods:
-
Session cookies — for the web app. Register or login →
pasteking_sessionHttpOnly cookie is set. Session expires after 72 hours (configurable viaSESSION_MAX_AGE_HOURS). -
Bearer tokens — for API/CLI usage. Create an API token from the web dashboard or via
POST /v1/auth/tokens. UseAuthorization: Bearer pk_...header.
Content is stored using a hybrid strategy:
- ≤ 64 KB (default
STORAGE_THRESHOLD): stored inline in PostgreSQL - > 64 KB: stored in MinIO/S3 object storage, with a
contentRefpointer in the DB
This keeps the database fast for small pastes while supporting arbitrarily large content via object storage.
Copy .env.example to .env and configure:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
REDIS_URL |
redis://localhost:6379 |
Redis for BullMQ queues & caching |
SESSION_SECRET |
(dev default) | Secret for session validation (min 32 chars) |
SESSION_MAX_AGE_HOURS |
72 |
Session cookie lifetime in hours |
S3_ENDPOINT |
http://localhost:9000 |
MinIO/S3 endpoint |
S3_ACCESS_KEY |
pasteking |
S3 access key |
S3_SECRET_KEY |
pasteking123 |
S3 secret key |
S3_BUCKET |
pasteking |
S3 bucket name |
S3_REGION |
us-east-1 |
S3 region |
STORAGE_THRESHOLD |
65536 |
Byte threshold for object storage (64 KB) |
API_PORT |
4000 |
API server port |
API_URL |
http://localhost:4000 |
API base URL |
NEXT_PUBLIC_API_URL |
http://localhost:4000 |
API URL exposed to the frontend |
WORKER_CONCURRENCY |
5 |
BullMQ worker concurrency |
| Variable | Description |
|---|---|
GITHUB_CLIENT_ID |
GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
GitHub OAuth app client secret |
GOOGLE_CLIENT_ID |
Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret |
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY |
Stripe API secret key |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret |
STRIPE_PRO_PRICE_ID |
Stripe Price ID for Pro plan |
STRIPE_TEAM_PRICE_ID |
Stripe Price ID for Team plan |
- Paste modes: Code (80+ languages with syntax highlighting), Text, Log, Markdown
- Visibility: Public, Unlisted, Private
- Expiration: Auto-expire pastes after a configurable duration
- Burn after read: Self-destructing pastes on first view
- Revision history: Full version history with line-by-line diff viewer
- Forking: Fork any public paste to create your own copy
- Full-text search: PostgreSQL
tsvectorsearch with highlighted snippets across public, personal, and workspace scopes
- End-to-end encryption: AES-256-GCM via Web Crypto API — encryption key stays in the URL fragment and never reaches the server
- Secret scanning: Regex-based detection of AWS keys, GitHub tokens, private keys, JWTs, Stripe keys, and more. Warnings shown before publish
- Session auth: scrypt password hashing, HttpOnly session cookies, CSRF protection
- API tokens: Scoped Bearer tokens (
paste:create,paste:read:own,paste:delete:own) - OAuth: GitHub and Google login
- Workspaces: Multi-user teams with OWNER / ADMIN / MEMBER / VIEWER roles
- Workspace pastes: Assign pastes to workspaces with RBAC access control
- Real-time editing: WebSocket-based live collaboration with cursor sharing and presence
- Workspace audit logs: Track all membership changes, paste actions, and settings updates
- Admin dashboard: Reports, user management, abuse flags, moderation audit logs at
/admin - Reporting: Users can report pastes (SPAM, MALWARE, CREDENTIAL_EXPOSURE, etc.)
- Moderation actions: Hide, disable, or permanently delete reported content
- User status: ACTIVE / RESTRICTED / SUSPENDED with enforced access controls
- Abuse detection: Automated flags for secret scans, rate spikes, and repeated reports
- Stripe integration: Checkout Sessions, Customer Portal, webhook processing
- Plan tiers: Free / Pro / Team with usage-based quotas (paste size, storage, tokens, workspaces, members)
- Subscription lifecycle: FREE / TRIALING / ACTIVE / PAST_DUE / CANCELED states
- Admin reconciliation: Sync subscription state from Stripe on demand
- View tracking: Per-paste view counts (unique by IP hash) with 24h / 7d / 30d breakdowns
- User analytics: Total pastes, views, and forks
- Workspace analytics: Per-workspace usage and storage stats
# Set API token (from Dashboard → API Tokens)
pasteking auth set-token pk_abc123...
# Create a paste from inline text
pasteking paste create "Hello world"
# Create from file
pasteking paste file ./script.sh --mode CODE --language bash
# Pipe from stdin
cat error.log | pasteking paste stdin --mode LOG --title "crash log"
# Get paste metadata + content
pasteking paste get abc123
# Get share URL
pasteking paste url abc123
# Delete paste
pasteking paste delete abc123 --token <delete-token>
# JSON output for scripting
pasteking paste create "data" --jsonConfig is stored at:
- Linux/macOS:
~/.config/pasteking/config.json - Windows:
%APPDATA%/pasteking/config.json
Non-encrypted pastes are scanned for common secret patterns before publishing:
| Pattern | Example |
|---|---|
| AWS Access Key | AKIAIOSFODNN7EXAMPLE |
| GitHub Token | ghp_..., github_pat_... |
| Private Key | -----BEGIN RSA PRIVATE KEY----- |
| JWT | eyJhbG... |
| Slack Token | xoxb-... |
| Stripe Key | sk_live_..., sk_test_... |
| Bearer Token | Bearer eyJ... |
| Env Secrets | API_KEY=..., SECRET_KEY=... |
| Generic Secrets | password: "...", secret: "..." |
Scanning is best-effort (regex heuristics). False positives are possible. Encrypted pastes bypass scanning since the server never sees plaintext.
| Limit | Free | Pro | Team |
|---|---|---|---|
| Max paste size | 512 KB | 5 MB | 10 MB |
| Max raw upload | 1 MB | 10 MB | 25 MB |
| API tokens | 5 | 25 | 50 |
| Active pastes | 500 | 5,000 | 25,000 |
| Personal storage | 100 MB | 5 GB | 25 GB |
| Workspaces owned | 2 | 10 | 50 |
| Workspace members | 5 | 25 | 100 |
| Workspace storage | 100 MB | 5 GB | 25 GB |
| Max expiration | 30 days | Unlimited | Unlimited |
- Create a Stripe test account
- Create Products and Prices for Pro and Team plans
- Set
STRIPE_SECRET_KEY,STRIPE_PRO_PRICE_ID,STRIPE_TEAM_PRICE_IDin.env - For webhooks:
stripe listen --forward-to localhost:4000/v1/billing/webhookand setSTRIPE_WEBHOOK_SECRET
OAuth is optional — email/password auth works without any OAuth configuration.
- Go to Settings → Developer settings → OAuth Apps → New OAuth App
- Set Authorization callback URL to
http://localhost:4000/v1/auth/oauth/github/callback - Set
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETin.env
- Go to Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID
- Add
http://localhost:4000/v1/auth/oauth/google/callbackas an authorized redirect URI - Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin.env
PasteKing consists of four services that need to run together:
| Service | Runtime | Description | Default Port |
|---|---|---|---|
| API | Node.js | Express REST API + WebSocket | 4000 |
| Web | Node.js | Next.js SSR frontend | 3000 |
| Worker | Node.js | BullMQ background job processor | — |
| CLI | Node.js | Command-line tool (no server) | — |
All services also require:
| Dependency | Purpose | Required |
|---|---|---|
| PostgreSQL 16+ | Primary database | Yes |
| Redis 7+ | Job queues, rate limiting | Yes |
| S3 / MinIO | Object storage (>64 KB) | Yes |
At minimum, set these for every deployment method:
NODE_ENV=production
# Generate a real secret: openssl rand -base64 48
SESSION_SECRET=<random-string-at-least-32-chars>
# Database — use a managed PostgreSQL or your own server
DATABASE_URL=postgresql://user:password@host:5432/pasteking
# Redis — use a managed Redis or your own server
REDIS_URL=redis://:password@host:6379
# Object storage — S3, R2, DigitalOcean Spaces, or self-hosted MinIO
S3_ENDPOINT=https://s3.amazonaws.com
S3_ACCESS_KEY=<your-access-key>
S3_SECRET_KEY=<your-secret-key>
S3_BUCKET=pasteking
S3_REGION=us-east-1
# URLs — set to your actual domain
API_PORT=4000
API_URL=https://api.yourdomain.com
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
# Worker
WORKER_CONCURRENCY=5
STORAGE_THRESHOLD=65536Optional variables (OAuth, Stripe) are documented in the Environment Variables section above.
pnpm install --frozen-lockfile
pnpm buildBuild outputs:
| App | Output | Start command |
|---|---|---|
api |
apps/api/dist/ |
node apps/api/dist/index.js |
web |
apps/web/.next/ |
cd apps/web && npx next start -p 3000 |
worker |
apps/worker/dist/ |
node apps/worker/dist/index.js |
cli |
apps/cli/dist/ |
node apps/cli/dist/index.js |
Before starting the services for the first time, run migrations:
# Generate Prisma client
pnpm db:generate
# Apply all migrations to the production database
cd packages/db && DATABASE_URL=<your-production-url> npx prisma migrate deployNote: Use
prisma migrate deploy(notmigrate dev) in production. It applies pending migrations without generating new ones.
This is the simplest approach. A single docker-compose.prod.yml runs all PasteKing services plus infrastructure.
Create Dockerfile in the project root:
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
WORKDIR /app
# ── Install dependencies ──────────────────────────────────────────────
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json apps/api/
COPY apps/web/package.json apps/web/
COPY apps/worker/package.json apps/worker/
COPY apps/cli/package.json apps/cli/
COPY packages/config/package.json packages/config/
COPY packages/crypto/package.json packages/crypto/
COPY packages/db/package.json packages/db/
COPY packages/sdk/package.json packages/sdk/
COPY packages/storage/package.json packages/storage/
COPY packages/types/package.json packages/types/
COPY packages/validation/package.json packages/validation/
RUN pnpm install --frozen-lockfile
# ── Build ─────────────────────────────────────────────────────────────
FROM deps AS build
COPY . .
RUN pnpm db:generate && pnpm build
# ── API production image ──────────────────────────────────────────────
FROM base AS api
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages ./packages
COPY --from=build /app/apps/api ./apps/api
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "apps/api/dist/index.js"]
# ── Web production image ──────────────────────────────────────────────
FROM base AS web
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages ./packages
COPY --from=build /app/apps/web ./apps/web
ENV NODE_ENV=production
EXPOSE 3000
WORKDIR /app/apps/web
CMD ["npx", "next", "start", "-p", "3000"]
# ── Worker production image ───────────────────────────────────────────
FROM base AS worker
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages ./packages
COPY --from=build /app/apps/worker ./apps/worker
ENV NODE_ENV=production
CMD ["node", "apps/worker/dist/index.js"]services:
# ── Infrastructure ────────────────────────────────────────────────────
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: pasteking
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: pasteking
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U pasteking']
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redisdata:/data
healthcheck:
test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping']
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:latest
restart: unless-stopped
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
volumes:
- miniodata:/data
command: server /data --console-address ":9001"
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 10s
timeout: 5s
retries: 5
# ── Migrations (runs once then exits) ─────────────────────────────────
migrate:
build:
context: .
target: api
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://pasteking:${DB_PASSWORD}@postgres:5432/pasteking
command: npx prisma migrate deploy --schema packages/db/prisma/schema.prisma
restart: 'no'
# ── Application services ──────────────────────────────────────────────
api:
build:
context: .
target: api
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migrate:
condition: service_completed_successfully
ports:
- '4000:4000'
environment:
NODE_ENV: production
API_PORT: 4000
API_URL: ${API_URL}
DATABASE_URL: postgresql://pasteking:${DB_PASSWORD}@postgres:5432/pasteking
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
SESSION_SECRET: ${SESSION_SECRET}
S3_ENDPOINT: http://minio:9000
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: pasteking
S3_REGION: us-east-1
web:
build:
context: .
target: web
restart: unless-stopped
depends_on:
- api
ports:
- '3000:3000'
environment:
NODE_ENV: production
NEXT_PUBLIC_API_URL: ${API_URL}
worker:
build:
context: .
target: worker
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
NODE_ENV: production
DATABASE_URL: postgresql://pasteking:${DB_PASSWORD}@postgres:5432/pasteking
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
S3_ENDPOINT: http://minio:9000
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: pasteking
S3_REGION: us-east-1
volumes:
pgdata:
redisdata:
miniodata:# Infrastructure passwords
DB_PASSWORD=<strong-random-password>
REDIS_PASSWORD=<strong-random-password>
# Session
SESSION_SECRET=<random-string-at-least-32-chars>
# Object storage
S3_ACCESS_KEY=<minio-access-key>
S3_SECRET_KEY=<minio-secret-key>
# Public URL (set to your domain)
API_URL=https://api.yourdomain.com# Build and start everything
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build
# Check logs
docker compose -f docker-compose.prod.yml logs -f
# Seed database (optional, first time only)
docker compose -f docker-compose.prod.yml exec api npx tsx packages/db/prisma/seed.tsPut Nginx or Caddy in front to handle TLS and route traffic:
Caddy (automatic HTTPS):
yourdomain.com {
reverse_proxy localhost:3000
}
api.yourdomain.com {
reverse_proxy localhost:4000
}
Nginx:
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket support for live collaboration
location /v1/ws {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}For a traditional VPS (DigitalOcean, Hetzner, Linode, etc.) without Docker.
# Node.js 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt-get install -y nodejs
# pnpm
corepack enable && corepack prepare pnpm@9.15.4 --activate
# PostgreSQL 16
sudo apt-get install -y postgresql-16
# Redis 7
sudo apt-get install -y redis-server
# MinIO (or use an external S3 provider)
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/# PostgreSQL — create database and user
sudo -u postgres psql -c "CREATE USER pasteking WITH PASSWORD '<db-password>';"
sudo -u postgres psql -c "CREATE DATABASE pasteking OWNER pasteking;"
# Redis — set a password in /etc/redis/redis.conf
sudo sed -i 's/^# requirepass .*/requirepass <redis-password>/' /etc/redis/redis.conf
sudo systemctl restart redisgit clone <repo-url> /opt/pasteking
cd /opt/pasteking
pnpm install --frozen-lockfile
# Configure environment
cp .env.example .env
# Edit .env with production values (DATABASE_URL, REDIS_URL, SESSION_SECRET, S3_*, etc.)
# Set up database
pnpm db:generate
cd packages/db && npx prisma migrate deploy && cd ../..
# Build all packages
pnpm build/etc/systemd/system/pasteking-api.service:
[Unit]
Description=PasteKing API
After=network.target postgresql.service redis.service
[Service]
Type=simple
User=pasteking
WorkingDirectory=/opt/pasteking
EnvironmentFile=/opt/pasteking/.env
ExecStart=/usr/bin/node apps/api/dist/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target/etc/systemd/system/pasteking-web.service:
[Unit]
Description=PasteKing Web
After=network.target pasteking-api.service
[Service]
Type=simple
User=pasteking
WorkingDirectory=/opt/pasteking/apps/web
EnvironmentFile=/opt/pasteking/.env
ExecStart=/usr/bin/npx next start -p 3000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target/etc/systemd/system/pasteking-worker.service:
[Unit]
Description=PasteKing Worker
After=network.target postgresql.service redis.service
[Service]
Type=simple
User=pasteking
WorkingDirectory=/opt/pasteking
EnvironmentFile=/opt/pasteking/.env
ExecStart=/usr/bin/node apps/worker/dist/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo useradd --system --no-create-home pasteking
sudo chown -R pasteking:pasteking /opt/pasteking
sudo systemctl daemon-reload
sudo systemctl enable pasteking-api pasteking-web pasteking-worker
sudo systemctl start pasteking-api pasteking-web pasteking-worker
# Check status
sudo systemctl status pasteking-api
sudo journalctl -u pasteking-api -fThen set up Nginx or Caddy as described above in the Reverse proxy section.
Deploy the Next.js frontend to Vercel and host the API + worker separately.
Important: Vercel only hosts the
webapp. Theapiandworkermust run on a separate server since they require persistent processes, WebSocket connections, and BullMQ job processing.
┌─────────────┐ ┌─────────────────┐ ┌────────────┐
│ Vercel │────▶│ VPS / Railway │────▶│ PostgreSQL │
│ (Next.js) │ │ (API + Worker) │ │ Redis │
└─────────────┘ └─────────────────┘ │ S3 / MinIO │
└────────────┘
Use any of these hosting options for the API and worker:
- Railway / Render / Fly.io — container-based PaaS
- DigitalOcean App Platform — managed container hosting
- Any VPS — use the Docker or manual method above
Set the environment variables for the API and worker services pointing to your managed database and Redis.
# Install Vercel CLI
npm i -g vercel
# From the project root
vercel --cwd apps/webOr connect the GitHub repo to Vercel with these settings:
| Setting | Value |
|---|---|
| Framework | Next.js |
| Root Directory | apps/web |
| Build Command | cd ../.. && pnpm install && pnpm build |
| Output Directory | .next |
Set the environment variable in Vercel's dashboard:
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
Ensure the API allows requests from your Vercel domain. The API already reads API_URL for CORS origin. Set API_URL on your API server to match the Vercel domain, or update the CORS config to include it.
Railway can host all services (API, web, worker) plus managed PostgreSQL and Redis.
npm i -g @railway/cli
railway login
railway initIn the Railway dashboard, add:
- PostgreSQL plugin → copies
DATABASE_URLautomatically - Redis plugin → copies
REDIS_URLautomatically
For object storage, use Cloudflare R2, AWS S3, or DigitalOcean Spaces (Railway doesn't offer S3).
| Service | Start Command | Root Directory |
|---|---|---|
api |
node apps/api/dist/index.js |
/ |
web |
cd apps/web && npx next start |
/ |
worker |
node apps/worker/dist/index.js |
/ |
Each service uses the same build command:
pnpm install --frozen-lockfile && pnpm db:generate && pnpm buildIn Railway's shared variables (or per-service):
NODE_ENV=production
SESSION_SECRET=<random-string>
S3_ENDPOINT=<your-s3-endpoint>
S3_ACCESS_KEY=<key>
S3_SECRET_KEY=<secret>
S3_BUCKET=pasteking
S3_REGION=us-east-1
API_URL=https://<api-service>.railway.app
NEXT_PUBLIC_API_URL=https://<api-service>.railway.apprailway run --service api -- npx prisma migrate deploy --schema packages/db/prisma/schema.prismaFly.io is well-suited for this project — it supports multi-process apps, WebSocket connections, and persistent volumes.
curl -L https://fly.io/install.sh | sh
fly auth loginapp = "pasteking-api"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
build-target = "api"
[env]
NODE_ENV = "production"
API_PORT = "4000"
[http_service]
internal_port = 4000
force_https = true
[[http_service.checks]]
path = "/health"
interval = 15000
timeout = 5000# PostgreSQL
fly postgres create --name pasteking-db
fly postgres attach pasteking-db --app pasteking-api
# Redis (Upstash)
fly redis create --name pasteking-redisfly secrets set \
SESSION_SECRET="<random-string>" \
S3_ENDPOINT="<endpoint>" \
S3_ACCESS_KEY="<key>" \
S3_SECRET_KEY="<secret>" \
S3_BUCKET="pasteking" \
S3_REGION="us-east-1" \
--app pasteking-apifly deploy --app pasteking-apiRepeat with separate fly.toml files for pasteking-web (target web, port 3000) and pasteking-worker (target worker, no http_service).
Coolify is an open-source, self-hosted alternative to Heroku/Vercel. Install it on any VPS and deploy PasteKing with a Git push.
- Install Coolify on your VPS:
curl -fsSL https://get.coolify.io | bash - Connect your GitHub/GitLab repository
- Create three services pointing to the same repo, each with a different Docker build target (
api,web,worker) - Add PostgreSQL and Redis resources from Coolify's one-click services
- Set the environment variables in each service's settings
- Deploy — Coolify handles TLS, reverse proxy, and zero-downtime deployments
PasteKing works with any S3-compatible object storage. Here are common options:
| Provider | S3_ENDPOINT |
Notes |
|---|---|---|
| AWS S3 | https://s3.amazonaws.com |
Set S3_REGION accordingly |
| Cloudflare R2 | https://<account-id>.r2.cloudflarestorage.com |
No egress fees |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
Included with droplets |
| MinIO (self-hosted) | http://minio:9000 |
Used in Docker Compose |
| Backblaze B2 | https://s3.<region>.backblazeb2.com |
Cheapest storage |
| Wasabi | https://s3.<region>.wasabisys.com |
No egress fees |
The API exposes health check endpoints for use with load balancers and monitoring tools:
| Endpoint | Purpose |
|---|---|
GET /health |
Returns 200 OK if the server is running |
GET /v1/health |
Returns 200 OK with version info |
Recommended monitoring setup:
- Uptime: Use UptimeRobot, BetterStack, or similar to ping
/healthevery 60 seconds - Logs: The API uses Pino structured JSON logging — pipe to your log aggregator (Datadog, Grafana Loki, etc.)
- Errors: Set
NODE_ENV=productionto suppress stack traces in API responses
For support, feature requests, or contributions, please open an issue or submit a pull request on GitHub. For commercial inquiries or custom deployments, contact me at:
- Email: ardenbimasaputra@gmail.com
- Website: https://pstkng.com
- Telegram: @ardeanbimasaputra
MIT
