Skip to main content

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:

TriggerDescription
subscriber_addedNew email signup
order_completedPurchase confirmation
order_shippedShipping notification
abandoned_cartCart 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. 1 hour: "You left something behind"
  2. 24 hours: "Still thinking about it?"
  3. 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

  1. Create service directory:
mkdir services/my-service
cd services/my-service
  1. Initialize package.json:
{
"name": "my-service",
"type": "module",
"scripts": {
"start": "tsx index.ts",
"build": "tsc"
},
"dependencies": {
"@supabase/supabase-js": "^2.x",
"tsx": "^4.x"
}
}
  1. 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);
  1. 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.