Skip to main content

Overview

This guide covers deploying Your Finance App to production, including environment configuration, database migrations, monitoring, and performance optimization.

Environment Configuration

Required Environment Variables

Create a production .env file with these required variables:
.env
# Database Configuration
DATABASE_URL="postgresql://user:password@host:6543/db?pgbouncer=true"
DIRECT_URL="postgresql://user:password@host:5432/db"

# JWT Configuration
JWT_SECRET="your-random-32-char-secret-key-here"
JWT_EXPIRES_IN="7d"

# Application
NODE_ENV="production"
PORT="3000"

# Frontend URL (for CORS)
FRONTEND_URL="https://your-frontend-domain.com"
Never use default or weak secrets in production. Generate strong secrets using:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Environment Variable Security

1

Use a Secret Manager

Store sensitive variables in:
  • AWS Secrets Manager
  • Google Cloud Secret Manager
  • Azure Key Vault
  • Railway/Vercel encrypted environment variables
2

Never Commit Secrets

Add to .gitignore:
.gitignore
.env
.env.local
.env.production
.env.*.local
3

Use .env.example as Template

Provide a template for required variables:
.env.example
DATABASE_URL="postgresql://..."
JWT_SECRET="change-me-in-production"
4

Validate on Startup

Add validation in your app:
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', 'FRONTEND_URL'];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}

Database Setup

Your Finance App is configured for Supabase PostgreSQL:
1

Create Supabase Project

  1. Go to supabase.com
  2. Create a new project
  3. Wait for database provisioning
2

Get Connection Strings

Navigate to Settings → Database and copy:
  • Transaction Mode (port 6543) → DATABASE_URL
  • Session Mode (port 5432) → DIRECT_URL
# Transaction mode - for queries (pooled)
DATABASE_URL="postgresql://postgres.xxxxx:[email protected]:6543/postgres?pgbouncer=true"

# Session mode - for migrations (direct)
DIRECT_URL="postgresql://postgres.xxxxx:[email protected]:5432/postgres"
3

Configure Prisma

Your schema.prisma is already configured:
schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}
Supabase uses connection pooling (PgBouncer) on port 6543 for better performance. Migrations require direct connection on port 5432.

Database Migrations in Production

Always backup your database before running migrations in production!

Safe Migration Process

# 1. Review pending migrations
pnpm prisma migrate status

# 2. Generate a deployment script (dry run)
pnpm prisma migrate deploy --preview-feature

# 3. Backup database (Supabase Dashboard → Database → Backups)

# 4. Deploy migrations
pnpm prisma migrate deploy

# 5. Verify schema
pnpm prisma db pull
pnpm prisma validate

Migration Strategy

Safe to deploy directly:
  • Adding new tables
  • Adding nullable columns
  • Adding indexes
  • Creating new enums
pnpm prisma migrate deploy
Require careful planning:
  • Renaming columns
  • Deleting columns
  • Changing column types
  • Adding non-nullable columns
Use a multi-step migration:
  1. Add new column (nullable)
  2. Backfill data
  3. Make non-nullable
  4. Remove old column
If a migration fails:
# Mark migration as rolled back
pnpm prisma migrate resolve --rolled-back "20231201000000_migration_name"

# Restore from backup
# (Use Supabase Dashboard → Database → Backups)

# Fix migration issue
# Create new migration
pnpm prisma migrate dev

Initial Data Seeding

The app includes a seed script for default categories:
apps/backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // Seed default categories, demo user, etc.
  console.log('Seeding database...');
  
  // Example: Create default categories
  await prisma.category.createMany({
    data: DEFAULT_CATEGORIES
  });
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());
Run the seed:
pnpm --filter backend prisma db seed

Deployment Platforms

Railway provides easy deployment with automatic builds:
1

Connect Repository

  1. Go to railway.app
  2. Create new project
  3. Connect your GitHub repository
2

Configure Service

Set build and start commands:
package.json
{
  "scripts": {
    "build": "pnpm --filter backend build",
    "start": "pnpm --filter backend start:prod"
  }
}
3

Add Environment Variables

In Railway dashboard, add all required environment variables from your .env.example
4

Deploy

Railway automatically deploys on push to main branch

Vercel (Frontend)

Deploy the frontend to Vercel:
# Install Vercel CLI
npm i -g vercel

# Deploy from frontend directory
cd apps/frontend
vercel

# Set environment variables
vercel env add VITE_API_URL production
Configure vercel.json:
apps/frontend/vercel.json
{
  "buildCommand": "pnpm build",
  "outputDirectory": "dist",
  "framework": "vite",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

Docker Deployment

While Your Finance App doesn’t include a Dockerfile by default, here’s a production-ready setup:
Dockerfile
# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Install pnpm
RUN npm install -g pnpm

# Copy package files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/backend/package.json ./apps/backend/

# Install dependencies
RUN pnpm install --frozen-lockfile

# Copy source code
COPY apps/backend ./apps/backend

# Generate Prisma client
RUN pnpm --filter backend prisma generate

# Build application
RUN pnpm --filter backend build

# Production stage
FROM node:18-alpine

WORKDIR /app

RUN npm install -g pnpm

# Copy built files and dependencies
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/backend/dist ./dist
COPY --from=builder /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=builder /app/apps/backend/prisma ./prisma

# Expose port
EXPOSE 3000

# Start application
CMD ["node", "dist/main.js"]
Build and run:
# Build image
docker build -t your-finance-app .

# Run container
docker run -p 3000:3000 \
  -e DATABASE_URL="postgresql://..." \
  -e JWT_SECRET="your-secret" \
  your-finance-app

Docker Compose

For local production testing:
docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - DIRECT_URL=${DIRECT_URL}
      - JWT_SECRET=${JWT_SECRET}
      - NODE_ENV=production
    depends_on:
      - postgres

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=finance
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:
Run with:
docker-compose up -d

Monitoring and Logging

Application Logging

The app uses a custom logger utility:
apps/backend/src/common/utils/logger.util.ts
export class AppLogger {
  constructor(private context: string) {}

  log(message: string) {
    console.log(`[${this.context}] ${message}`);
  }

  logOperation(operation: string, data: any) {
    console.log(`[${this.context}] ${operation}:`, JSON.stringify(data));
  }

  logSuccess(operation: string, data: any) {
    console.log(`[${this.context}] ✓ ${operation}:`, JSON.stringify(data));
  }

  logFailure(operation: string, error: Error) {
    console.error(`[${this.context}] ✗ ${operation}:`, error.message);
  }
}
Usage in services:
private readonly logger = new AppLogger(TransactionsService.name);

async create(dto: CreateTransactionDto, userId: string) {
  this.logger.logOperation('Create transaction', { type: dto.type, amount: dto.amount });
  
  try {
    const result = await this.doCreate(dto, userId);
    this.logger.logSuccess('Create transaction', { id: result.id });
    return result;
  } catch (error) {
    this.logger.logFailure('Create transaction', error);
    throw error;
  }
}

Error Tracking

Integrate error tracking services:
pnpm add @sentry/node @sentry/tracing
main.ts
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0
});

Health Checks

Implement health check endpoints:
apps/backend/src/app.controller.ts
@Controller()
export class AppController {
  @Get('health')
  healthCheck() {
    return {
      status: 'ok',
      timestamp: new Date().toISOString(),
      uptime: process.uptime()
    };
  }

  @Get('health/db')
  async databaseHealth() {
    try {
      await this.prisma.$queryRaw`SELECT 1`;
      return { status: 'ok', database: 'connected' };
    } catch (error) {
      return { status: 'error', database: 'disconnected' };
    }
  }
}

Performance Optimization

Enable Production Mode

NODE_ENV=production pnpm start:prod
This enables:
  • Optimized builds
  • Disabled debug logs
  • Better error handling
  • Performance optimizations

Database Connection Pooling

Optimize Prisma connection settings:
DATABASE_URL="postgresql://user:password@host:6543/db?pgbouncer=true&connection_limit=10&pool_timeout=20"
Connection pooling limits are based on your plan:
  • Free tier: 10 connections
  • Pro tier: 50 connections
  • Enterprise: Custom

Response Compression

Enable compression middleware:
main.ts
import compression from 'compression';

app.use(compression());

Rate Limiting

Protect against abuse:
pnpm add @nestjs/throttler
app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,      // Time window in seconds
      limit: 100    // Max requests per window
    })
  ]
})
export class AppModule {}
Apply to endpoints:
@UseGuards(ThrottlerGuard)
@Post('login')
login() {}

Caching Headers

Set appropriate cache headers:
@Header('Cache-Control', 'public, max-age=300')
@Get('categories')
getCategories() {
  // Cached for 5 minutes
}

Security Hardening

Helmet for Security Headers

pnpm add helmet
main.ts
import helmet from 'helmet';

app.use(helmet());
This adds:
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security
  • And more security headers

CORS Configuration

apps/backend/src/main.ts
const app = await NestFactory.create(AppModule, {
  cors: {
    origin: process.env.FRONTEND_URL,
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
    credentials: true,
    allowedHeaders: 'Content-Type,Authorization'
  }
});

SSL/TLS

Always use HTTPS in production:
  • Most platforms (Railway, Vercel, etc.) provide free SSL certificates
  • For custom domains, use Let’s Encrypt
  • Enforce HTTPS redirects

Deployment Checklist

Troubleshooting

Common Issues

Symptoms: Can't reach database server or Connection timeoutSolutions:
  1. Verify DATABASE_URL is correct
  2. Check database is accessible from your deployment platform
  3. Ensure connection string includes ?pgbouncer=true for Supabase
  4. Check IP whitelist in database settings
Symptoms: Migration failed to applySolutions:
  1. Use DIRECT_URL for migrations (port 5432, not 6543)
  2. Check migration files for syntax errors
  3. Verify database user has sufficient permissions
  4. Review migration logs for specific error messages
Symptoms: Invalid token or Token expiredSolutions:
  1. Ensure JWT_SECRET is set and consistent
  2. Verify JWT_EXPIRES_IN is valid format (e.g., ‘7d’)
  3. Check system time is synchronized
  4. Clear old tokens and re-authenticate
Symptoms: Browser shows CORS policy errorsSolutions:
  1. Set FRONTEND_URL to your actual frontend domain
  2. Ensure credentials: true is set if using cookies
  3. Check allowedHeaders includes required headers
  4. Verify OPTIONS requests are handled

Monitoring Production

Key Metrics to Track

Response Time

Monitor API endpoint response times. Target: < 200ms for most endpoints.

Error Rate

Track 4xx and 5xx errors. Target: < 1% error rate.

Database Performance

Monitor query times and connection pool usage.

Resource Usage

Track CPU, memory, and disk usage. Scale when consistently > 70%.

Logging Best Practices

// ✅ Good - Structured logging
this.logger.logOperation('Create transaction', {
  userId,
  amount: dto.amount,
  type: dto.type,
  timestamp: new Date().toISOString()
});

// ❌ Bad - Unstructured logging
console.log('Creating transaction for user with amount ' + dto.amount);

Advanced Concepts

Technical patterns and architecture

Best Practices

Code quality and testing strategies

Supabase Docs

Database setup and configuration

Railway Docs

Deployment platform documentation

Build docs developers (and LLMs) love