Skip to main content

Background Services Deployment

Background services are deployed to Railway for scheduled job execution.

Railway Setup

Project Structure

Each service is deployed as a separate Railway service:

artbase-services (Railway Project)
├── etsy-sync
├── gumroad-sync
├── analytics-aggregator
├── scheduled-send
├── automation-processor
├── guest-cleanup
└── cart-abandonment

Initial Setup

  1. Create Railway Project

    # Install Railway CLI
    npm i -g @railway/cli

    # Login
    railway login

    # Create project
    railway init
  2. Link Services

    cd services/etsy-sync
    railway link

Service Configuration

Environment Variables

Configure in Railway Dashboard → Service → Variables:

# Required for all services
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Service-specific
ENCRYPTION_KEY=32-byte-hex-key
RESEND_API_KEY=re_...

railway.json

Each service needs a configuration file:

{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}

Cron Schedule

Configure in Railway Dashboard → Service → Settings → Cron:

ServiceScheduleDescription
etsy-sync*/5 * * * *Every 5 minutes
gumroad-sync*/5 * * * *Every 5 minutes
analytics-aggregator0 * * * *Every hour
scheduled-send* * * * *Every minute
automation-processor* * * * *Every minute
guest-cleanup0 0 * * *Daily at midnight
cart-abandonment*/15 * * * *Every 15 minutes

Deployment Process

Manual Deployment

# Deploy single service
cd services/etsy-sync
railway up

# Deploy with specific environment
railway up --environment production

Automatic Deployment

Configure GitHub integration:

  1. Railway Dashboard → Project → Settings → Git
  2. Connect GitHub repository
  3. Select branch for auto-deploy
  4. Configure root directory per service

Deployment Workflow

# .github/workflows/deploy-services.yml
name: Deploy Services

on:
push:
branches: [main]
paths:
- 'services/**'

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Railway CLI
run: npm i -g @railway/cli

- name: Deploy etsy-sync
if: contains(github.event.commits.*.modified, 'services/etsy-sync')
run: |
cd services/etsy-sync
railway up --service etsy-sync
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Service Patterns

Basic Service Structure

// services/example/index.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);

async function main() {
console.log(`[${new Date().toISOString()}] Service starting...`);

try {
// Service logic here
await processJobs();

console.log(`[${new Date().toISOString()}] Service completed successfully`);
} catch (error) {
console.error(`[${new Date().toISOString()}] Service failed:`, error);
process.exit(1);
}
}

main();

With Retry Logic

async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;

const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}

Rate Limiting

class RateLimiter {
private queue: (() => Promise<void>)[] = [];
private processing = false;

constructor(private requestsPerSecond: number) {}

async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
resolve(await fn());
} catch (error) {
reject(error);
}
});
this.process();
});
}

private async process() {
if (this.processing) return;
this.processing = true;

while (this.queue.length > 0) {
const fn = this.queue.shift()!;
await fn();
await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond));
}

this.processing = false;
}
}

Monitoring

Logs

View logs in Railway Dashboard → Service → Logs

Or via CLI:

railway logs --service etsy-sync

Log Format

Use structured logging:

function log(level: string, message: string, data?: object) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
...data,
}));
}

log('info', 'Processing orders', { count: 5, accountId: 'abc123' });

Metrics

Track execution metrics:

async function main() {
const startTime = Date.now();

try {
const result = await processJobs();

log('info', 'Service completed', {
duration_ms: Date.now() - startTime,
processed: result.count,
errors: result.errors,
});
} catch (error) {
log('error', 'Service failed', {
duration_ms: Date.now() - startTime,
error: error.message,
});
process.exit(1);
}
}

Scaling

Horizontal Scaling

For high-volume processing, use multiple instances:

  1. Railway Dashboard → Service → Settings
  2. Increase replica count
  3. Implement job locking to prevent duplicates

Job Locking

async function acquireLock(jobId: string): Promise<boolean> {
const { data, error } = await supabase
.from('job_locks')
.insert({ job_id: jobId, locked_at: new Date().toISOString() })
.select()
.single();

return !error;
}

async function releaseLock(jobId: string) {
await supabase
.from('job_locks')
.delete()
.eq('job_id', jobId);
}

Troubleshooting

Service Not Running

  1. Check cron schedule syntax
  2. Verify environment variables
  3. Review deployment logs
  4. Check Railway service status

Database Connection Issues

// Add connection retry
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
db: {
schema: 'public',
},
global: {
headers: { 'x-my-custom-header': 'service-worker' },
},
}
);

Memory Issues

If service runs out of memory:

  1. Increase memory in Railway settings
  2. Process data in smaller batches
  3. Stream large datasets instead of loading all at once
// Batch processing
async function processBatch(items: any[], batchSize = 100) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await Promise.all(batch.map(processItem));
}
}