Skip to main content

Deployment Guide

Bilko — Deployment Guide

Status: PLANNED (not deployed yet)

This document describes the target deployment architecture for Bilko. Infrastructure has not yet been provisioned.


Overview

Bilko uses a multi-service architecture deployed to different platforms optimized for each component:

Component Platform Status
Frontend (Next.js 15) Vercel PLANNED
Backend (Express + PostgreSQL) Railway PLANNED
File Storage (Receipts, PDFs) Cloudflare R2 PLANNED
Email (Transactional) SendGrid PLANNED
DNS Cloudflare bilko.io registered, DNS not configured

Target Domains:

  • Primary: bilko.io (all regions)
  • Redirect: bilko.rs (Serbia market, 301 redirect to bilko.io)

Deployment Architecture Overview

graph TD
    User["User Browser / Mobile PWA"]
    DNS["Cloudflare DNS\nbilko.io / bilko.rs"]
    CF_PROXY["Cloudflare Proxy\nDDoS + SSL Termination"]
    Vercel["Vercel Edge Network\nNext.js 15 Frontend\nbilko.io"]
    Railway["Railway EU West\nExpress API\napi.bilko.io"]
    PG["PostgreSQL 15\nRailway Managed DB"]
    R2["Cloudflare R2\nbilko-receipts bucket\n(Receipts + PDFs)"]
    SG["SendGrid\nTransactional Email\[email protected]"]

    User --> DNS
    DNS --> CF_PROXY
    CF_PROXY --> Vercel
    CF_PROXY --> Railway
    Railway --> PG
    Railway --> R2
    Railway --> SG
    Vercel -->|"HTTPS API calls"| Railway

Environment Topology

graph LR
    subgraph LOCAL["Local (Developer)"]
        L_WEB["localhost:3000\nNext.js dev"]
        L_API["localhost:4000\nExpress dev"]
        L_DB["localhost:5432\nbilko_dev DB"]
    end

    subgraph STAGING["Staging"]
        S_WEB["bilko-pr-{n}.vercel.app\nVercel Preview"]
        S_API["staging Railway env\nbilko-api-staging"]
        S_DB["Railway PostgreSQL\nbilko_staging DB"]
    end

    subgraph PROD["Production"]
        P_WEB["bilko.io\nVercel Production"]
        P_API["api.bilko.io\nRailway Production"]
        P_DB["Railway PostgreSQL\nbilko_prod DB"]
        P_R2["Cloudflare R2\nbilko-receipts"]
    end

    L_WEB --> L_API --> L_DB
    S_WEB --> S_API --> S_DB
    P_WEB --> P_API --> P_DB
    P_API --> P_R2

Frontend Deployment (Vercel)

Platform: Vercel

  • Why: Zero-config Next.js deployment, edge CDN, automatic HTTPS, preview deployments
  • Pricing: Free tier for MVP (handles 10K+ users)
  • Region: Global Edge Network (CDN)

Deployment Process

Initial Setup

  1. Install Vercel CLI:

    npm install -g vercel
    
  2. Login:

    vercel login
    
  3. Link project:

    cd apps/web
    vercel link
    
  4. Deploy to production:

    vercel --prod
    

Environment Variables (Vercel Dashboard)

Required environment variables for production:

NEXT_PUBLIC_API_URL=https://api.bilko.io
NEXT_PUBLIC_APP_ENV=production

Custom Domain Setup

  1. Go to Vercel Dashboard → Project Settings → Domains
  2. Add custom domain: bilko.io
  3. Configure DNS (see DNS section below)
  4. Vercel auto-provisions SSL certificate

Preview Deployments

  • Every PR automatically deploys to unique URL: bilko-pr-{number}.vercel.app
  • Allows testing before merging to main
  • Automatically deleted after PR merge/close

Backend Deployment (Railway)

Platform: Railway

  • Why: PostgreSQL included, simple deployment, EU region available (GDPR), affordable
  • Pricing: ~€20/mo for MVP (starter tier: 2GB RAM, 2 vCPU, 10GB storage)
  • Region: EU West (Frankfurt or Paris — GDPR compliant)

Deployment Process

Initial Setup

  1. Install Railway CLI:

    npm install -g @railway/cli
    
  2. Login:

    railway login
    
  3. Create project:

    railway init
    
  4. Provision PostgreSQL:

    railway add --database postgresql
    
  5. Deploy API:

    cd apps/api
    railway up
    

Environment Variables (Railway Dashboard)

Required environment variables for production:

# Database (auto-provisioned by Railway)
DATABASE_URL=${{Postgres.DATABASE_URL}}

# JWT Secrets (generate with: openssl rand -base64 32)
JWT_SECRET=<your-secret-here>
JWT_REFRESH_SECRET=<your-refresh-secret-here>

# SendGrid Email
SENDGRID_API_KEY=<your-sendgrid-key>

# Cloudflare R2 Storage
R2_ACCESS_KEY_ID=<your-r2-key>
R2_SECRET_ACCESS_KEY=<your-r2-secret>
R2_BUCKET_NAME=bilko-receipts
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com

# App Config
PORT=4000
NODE_ENV=production
ALLOWED_ORIGINS=https://bilko.io,https://www.bilko.io

Database Migrations

Run Prisma migrations on Railway:

railway run npx prisma migrate deploy

Monitoring

Railway provides built-in monitoring:

  • CPU, memory, network usage
  • Logs: railway logs
  • Metrics: Railway Dashboard → Metrics tab

File Storage (Cloudflare R2)

Platform: Cloudflare R2

  • Why: Zero egress fees (S3 charges for downloads), S3-compatible API
  • Pricing: ~€1/mo for MVP (storage: €0.015/GB, no egress fees)
  • Use Cases: Receipt photos (JPG, PNG), invoice PDFs, expense attachments

Setup Process

1. Create R2 Bucket

  1. Go to Cloudflare Dashboard → R2
  2. Create bucket: bilko-receipts
  3. Note Account ID from dashboard

2. Generate API Credentials

  1. R2 → Manage R2 API Tokens
  2. Create API token with permissions:
    • Object Read & Write
    • Bucket scope: bilko-receipts
  3. Save Access Key ID and Secret Access Key

3. Configure CORS

Allow uploads from frontend:

{
  "AllowedOrigins": ["https://bilko.io", "https://www.bilko.io"],
  "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
  "AllowedHeaders": ["*"],
  "MaxAgeSeconds": 3600
}

Apply via Cloudflare Dashboard → R2 → bilko-receipts → Settings → CORS

4. Backend Integration

Use AWS SDK for S3 (R2 is S3-compatible):

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3Client = new S3Client({
  region: "auto",
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

DNS Configuration (Cloudflare)

DNS + Traffic Routing Flow

flowchart TD
    REQ["Incoming Request"]
    CHK_RS{{"bilko.rs\nor www.bilko.rs?"}}
    REDIRECT["301 Permanent Redirect\nhttps://bilko.io$request.uri"]
    CHK_API{{"api.bilko.io?"}}
    VERCEL_CNAME["CNAME → cname.vercel-dns.com\n(Proxied via Cloudflare)"]
    RAIL_CNAME["CNAME → railway.app\n(DNS only, no proxy)"]
    SSL_VERCEL["Vercel SSL\n(Let's Encrypt auto-renew)"]
    SSL_RAIL["Railway SSL\n(Let's Encrypt auto-renew)"]

    REQ --> CHK_RS
    CHK_RS -->|Yes| REDIRECT
    CHK_RS -->|No| CHK_API
    CHK_API -->|Yes| RAIL_CNAME --> SSL_RAIL
    CHK_API -->|No| VERCEL_CNAME --> SSL_VERCEL

Domains

  • bilko.io — Primary domain (registered, DNS not configured)
  • bilko.rs — Serbia redirect (not yet registered)

DNS Records (Cloudflare Dashboard)

bilko.io

Type Name Value Proxy
CNAME @ cname.vercel-dns.com Yes
CNAME www cname.vercel-dns.com Yes
CNAME api .railway.app No

bilko.rs (future)

Type Name Value Proxy
CNAME @ bilko.io Yes (301 redirect rule)

Redirect Rules (Cloudflare)

Create redirect rule for bilko.rs → bilko.io:

  • If: Hostname equals bilko.rs or www.bilko.rs
  • Then: Dynamic redirect to https://bilko.io$request.uri (301 permanent)

Email Configuration (SendGrid)

Platform: SendGrid

  • Why: Free tier (100 emails/day), email templates, delivery tracking
  • Pricing: €0 for MVP (upgrade to €15/mo for 40K emails when needed)

Setup Process

1. Create SendGrid Account

  1. Sign up at sendgrid.com
  2. Verify email address
  3. Create API key with Mail Send permissions

2. Domain Authentication

  1. SendGrid Dashboard → Settings → Sender Authentication
  2. Authenticate domain: bilko.io
  3. Add DNS records to Cloudflare (CNAME for DKIM, TXT for SPF)

3. Email Templates

Create transactional email templates:

  • Welcome email (user registration)
  • Invoice sent notification
  • Payment received confirmation
  • Password reset
  • 2FA code

4. Backend Integration

Use SendGrid Node.js SDK:

import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

await sgMail.send({
  to: "[email protected]",
  from: "[email protected]",
  subject: "Invoice #12345",
  templateId: "d-abc123...",
  dynamicTemplateData: { invoiceNumber: "12345" },
});

Cost Estimate

MVP Budget (Monthly)

Service Tier Cost
Vercel (Frontend) Hobby €0
Railway (Backend + DB) Starter (2GB RAM) €20
Cloudflare R2 (Storage) Pay-as-you-go €1
SendGrid (Email) Free (100/day) €0
TOTAL €21/mo

Scale-up Budget (1K-10K users)

Service Tier Cost
Vercel (Frontend) Pro €20
Railway (Backend + DB) Pro (8GB RAM) €50
Cloudflare R2 (Storage) Pay-as-you-go €5
SendGrid (Email) Essentials (40K/mo) €15
TOTAL €90/mo

Deployment Checklist

Development Environment

  • Local PostgreSQL 15+ installed
  • Node.js 18+ installed
  • Environment variables in .env files
  • Database migrated: npx prisma migrate dev
  • Frontend runs: npm run dev (port 3000)
  • Backend runs: npm run dev (port 4000)

Staging Environment

  • Railway project created
  • PostgreSQL provisioned on Railway
  • Environment variables configured
  • Database migrated: railway run npx prisma migrate deploy
  • Backend deployed: railway up
  • Vercel project created
  • Frontend deployed: vercel
  • Custom domain configured: staging.bilko.io
  • End-to-end smoke test passed

Production Environment

  • DNS configured (bilko.io → Vercel + Railway)
  • SSL certificates provisioned (automatic)
  • Environment variables configured (production secrets)
  • Database migrated: railway run npx prisma migrate deploy
  • Cloudflare R2 bucket created
  • CORS configured on R2
  • SendGrid domain authenticated
  • Email templates created
  • Backend deployed: railway up --production
  • Frontend deployed: vercel --prod
  • Monitoring configured (Railway metrics + Sentry)
  • Backup strategy tested
  • Load testing completed (1K concurrent users)
  • Incident response plan documented

Rollback Strategy

Rollback Decision Flow

flowchart TD
    INC["Incident Detected"]
    TYPE{{"Affected\nComponent?"}}

    FE_ROLL["Vercel Dashboard\n→ Deployments\n→ Promote to Production\n⏱ Instant (no rebuild)"]
    BE_ROLL["Railway Dashboard\n→ Deployments\n→ Redeploy previous\n⏱ ~2 minutes"]
    DB_ROLL{{"Data\nCorrupted?"}}
    DB_FORWARD["Forward Fix\n(preferred)\nDeploy corrective migration"]
    DB_RESTORE["Railway Backup Restore\n→ Select snapshot\n→ Test integrity\n→ Switch traffic"]

    INC --> TYPE
    TYPE -->|"Frontend only"| FE_ROLL
    TYPE -->|"Backend only"| BE_ROLL
    TYPE -->|"Database"| DB_ROLL
    DB_ROLL -->|No| DB_FORWARD
    DB_ROLL -->|Yes| DB_RESTORE

Frontend Rollback (Vercel)

Vercel keeps all previous deployments. To rollback:

  1. Go to Vercel Dashboard → Deployments
  2. Find previous working deployment
  3. Click "Promote to Production"
  4. Instant rollback (no rebuild needed)

Backend Rollback (Railway)

Railway keeps deployment history. To rollback:

  1. Go to Railway Dashboard → Deployments
  2. Select previous deployment
  3. Click "Redeploy"
  4. Rollback completes in ~2 minutes

Database Rollback

CRITICAL: Database rollbacks are risky. Prefer forward-fixing.

If necessary:

  1. Restore from Railway automated backup
  2. Re-run migrations up to previous version
  3. Test data integrity before switching traffic

Prevention:

  • Always test migrations on staging first
  • Use backward-compatible changes
  • Never drop columns without multi-step migration plan

Monitoring & Alerts

Railway Monitoring

Built-in metrics:

  • CPU usage
  • Memory usage
  • Network I/O
  • Request rate
  • Error rate

Vercel Analytics

Built-in analytics:

  • Page views
  • Core Web Vitals
  • Geographic distribution
  • Device breakdown

Uptime Monitoring (Recommended: Uptime Robot)

Free tier monitors:

  • Frontend (bilko.io)
  • Backend (api.bilko.io/health)
  • Check interval: 5 minutes
  • Alert via email/Slack on downtime

Error Tracking (Recommended: Sentry)

Integrate Sentry for:

  • JavaScript errors (frontend)
  • API errors (backend)
  • Performance monitoring
  • Release tracking

Security Considerations

HTTPS Everywhere

  • Vercel: Automatic HTTPS
  • Railway: Automatic HTTPS
  • Cloudflare: Force HTTPS redirect

Secrets Management

  • NEVER commit secrets to git
  • Store in platform dashboards (Vercel, Railway)
  • Rotate JWT secrets quarterly
  • Rotate API keys annually

CORS Configuration

Restrict to production domains only:

const corsOptions = {
  origin: process.env.ALLOWED_ORIGINS.split(","),
  credentials: true,
};

Rate Limiting

Protect API endpoints:

  • General: 100 req/min per IP
  • Auth: 5 req/min per IP
  • Reports: 10 req/min per user

Backup & Disaster Recovery

Database Backups (Railway)

  • Automated: Daily backups, 30-day retention
  • Manual: Export via pg_dump before major migrations
  • Restore: Railway Dashboard → Database → Backups → Restore

File Backups (Cloudflare R2)

  • Strategy: Receipts/PDFs linked in database, R2 is source of truth
  • Retention: Indefinite (regulatory requirement for financial records)
  • Replication: Enable R2 multi-region replication (future)

Recovery Time Objective (RTO)

  • Target: 1 hour (from incident to service restored)
  • Components:
    • Database restore: 15 minutes
    • Backend redeploy: 5 minutes
    • Frontend redeploy: 2 minutes
    • DNS propagation: 5 minutes (cached)

Recovery Point Objective (RPO)

  • Target: 24 hours (maximum data loss acceptable)
  • Achieved via: Daily database backups


Last Updated: 2026-02-20 Status: PLANNED — No infrastructure deployed yet Next Steps: Provision Railway project + PostgreSQL, deploy staging environment