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
-
Create Railway Project
# Install Railway CLI
npm i -g @railway/cli
# Login
railway login
# Create project
railway init -
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:
| Service | Schedule | Description |
|---|---|---|
| etsy-sync | */5 * * * * | Every 5 minutes |
| gumroad-sync | */5 * * * * | Every 5 minutes |
| analytics-aggregator | 0 * * * * | Every hour |
| scheduled-send | * * * * * | Every minute |
| automation-processor | * * * * * | Every minute |
| guest-cleanup | 0 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:
- Railway Dashboard → Project → Settings → Git
- Connect GitHub repository
- Select branch for auto-deploy
- 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:
- Railway Dashboard → Service → Settings
- Increase replica count
- 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
- Check cron schedule syntax
- Verify environment variables
- Review deployment logs
- 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:
- Increase memory in Railway settings
- Process data in smaller batches
- 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));
}
}