Background Services
Artbase Studio uses Railway-hosted background services for asynchronous job processing. Each service runs independently and communicates through the database.
Service Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web App │────▶│ Supabase │◀────│ Services │
│ (Next.js) │ │ (PostgreSQL) │ │ (Railway) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
┌──────────┼──────────┐
│ │ │
┌─────▼─────┐ ┌──▼──┐ ┌─────▼─────┐
│ pg_notify │ │ RLS │ │ Triggers │
└───────────┘ └─────┘ └───────────┘
Available Services
etsy-sync
Synchronizes orders from connected Etsy shops.
Location: services/etsy-sync/
Responsibilities:
- Refresh OAuth tokens when expired
- Fetch new orders (receipts) from Etsy API
- Normalize Etsy data to unified order schema
- Create customers, orders, and order items
- Store raw JSON for auditing
Configuration:
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=xxx
ENCRYPTION_KEY=xxx
SYNC_INTERVAL_MS=300000 # 5 minutes
Cron: Runs every 5 minutes
gumroad-sync
Synchronizes sales from connected Gumroad accounts.
Location: services/gumroad-sync/
Responsibilities:
- Fetch sales via Gumroad API
- Normalize to unified order schema
- Handle digital product deliveries
- Track license keys
Cron: Runs every 5 minutes
analytics-aggregator
Aggregates analytics data for dashboard metrics.
Location: services/analytics-aggregator/
Responsibilities:
- Calculate daily/weekly/monthly revenue
- Aggregate product performance metrics
- Compute customer insights
- Generate inventory alerts
Cron: Runs hourly
Key Metrics:
interface AggregatedMetrics {
total_revenue: number;
order_count: number;
average_order_value: number;
top_products: ProductMetric[];
revenue_by_channel: Record<string, number>;
customer_acquisition: number;
repeat_customer_rate: number;
}
scheduled-send
Processes scheduled email campaigns.
Location: services/scheduled-send/
Responsibilities:
- Poll for campaigns with
scheduled_at <= now() - Fetch subscribers based on segment
- Send emails via Resend API
- Update campaign status and stats
Cron: Runs every minute
automation-processor
Executes email automation workflows.
Location: services/automation-processor/
Responsibilities:
- Process automation triggers (welcome, purchase, etc.)
- Evaluate delay conditions
- Send automated emails
- Track automation performance
Triggers:
| Trigger | Description |
|---|---|
subscriber_added | New email signup |
order_completed | Purchase confirmation |
order_shipped | Shipping notification |
abandoned_cart | Cart recovery (via cart-abandonment) |
guest-cleanup
Manages guest session expiration and notifications.
Location: services/guest-cleanup/
Responsibilities:
- Identify expiring guest sessions
- Send reminder emails (7 days, 3 days, 1 day before)
- Clean up expired guest data
- Support per-organization email settings
Cron: Runs daily
cart-abandonment
Handles abandoned cart recovery emails.
Location: services/cart-abandonment/
Responsibilities:
- Monitor abandoned carts (no purchase within 1 hour)
- Send recovery emails with cart contents
- Track recovery conversions
- Respect email frequency limits
Email Sequence:
- 1 hour: "You left something behind"
- 24 hours: "Still thinking about it?"
- 72 hours: "Last chance" (optional discount)
Shared Utilities
Common code lives in services/shared/:
Token Encryption
// services/shared/crypto.ts
import { encrypt, decrypt } from './crypto';
// Encrypt OAuth tokens before storage
const encryptedToken = encrypt(accessToken, process.env.ENCRYPTION_KEY);
// Decrypt when needed
const accessToken = decrypt(encryptedToken, process.env.ENCRYPTION_KEY);
Campaign Sending
// services/shared/send-campaign-helper.ts
import { sendCampaign } from './send-campaign-helper';
await sendCampaign({
campaignId: 'uuid',
supabase: supabaseClient,
});
Creating a New Service
- Create service directory:
mkdir services/my-service
cd services/my-service
- Initialize package.json:
{
"name": "my-service",
"type": "module",
"scripts": {
"start": "tsx index.ts",
"build": "tsc"
},
"dependencies": {
"@supabase/supabase-js": "^2.x",
"tsx": "^4.x"
}
}
- Create 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('Service starting...');
// Your service logic here
console.log('Service completed');
}
main().catch(console.error);
- Deploy to Railway:
railway init
railway up
Monitoring
Services log to Railway's logging system. Key metrics to monitor:
- Execution time: Alert if jobs take > 5 minutes
- Error rate: Alert on repeated failures
- Queue depth: Monitor pending jobs
- Memory usage: Services should stay under 512MB
Example log format:
[2024-01-15T10:30:00Z] INFO: etsy-sync starting
[2024-01-15T10:30:01Z] INFO: Processing 3 connected accounts
[2024-01-15T10:30:05Z] INFO: Account abc123: synced 5 new orders
[2024-01-15T10:30:10Z] INFO: etsy-sync completed in 10s
Error Handling
Services implement retry logic with exponential backoff:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000); // 1s, 2s, 4s
}
}
throw new Error('Max retries exceeded');
}
Rate limit errors (429) trigger longer backoff periods and are not counted against retry limits.