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
-
Install Vercel CLI:
npm install -g vercel -
Login:
vercel login -
Link project:
cd apps/web vercel link -
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
- Go to Vercel Dashboard → Project Settings → Domains
- Add custom domain:
bilko.io - Configure DNS (see DNS section below)
- 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
-
Install Railway CLI:
npm install -g @railway/cli -
Login:
railway login -
Create project:
railway init -
Provision PostgreSQL:
railway add --database postgresql -
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
- Go to Cloudflare Dashboard → R2
- Create bucket:
bilko-receipts - Note Account ID from dashboard
2. Generate API Credentials
- R2 → Manage R2 API Tokens
- Create API token with permissions:
- Object Read & Write
- Bucket scope:
bilko-receipts
- 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.rsorwww.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
- Sign up at sendgrid.com
- Verify email address
- Create API key with Mail Send permissions
2. Domain Authentication
- SendGrid Dashboard → Settings → Sender Authentication
- Authenticate domain:
bilko.io - 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
.envfiles - 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:
- Go to Vercel Dashboard → Deployments
- Find previous working deployment
- Click "Promote to Production"
- Instant rollback (no rebuild needed)
Backend Rollback (Railway)
Railway keeps deployment history. To rollback:
- Go to Railway Dashboard → Deployments
- Select previous deployment
- Click "Redeploy"
- Rollback completes in ~2 minutes
Database Rollback
CRITICAL: Database rollbacks are risky. Prefer forward-fixing.
If necessary:
- Restore from Railway automated backup
- Re-run migrations up to previous version
- 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_dumpbefore 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
Related Documents
- CI/CD Pipeline: CI-CD.md
- Environment Setup: ENVIRONMENT.md
- Security Architecture: ../security/SECURITY-ARCHITECTURE.md
- Testing Guide: ../testing/TESTING-GUIDE.md
Last Updated: 2026-02-20 Status: PLANNED — No infrastructure deployed yet Next Steps: Provision Railway project + PostgreSQL, deploy staging environment