Downcity
Downcity City DocsGuides

Hooks and Billing Logic

Connect quotas, usage, logs, and billing into the Action lifecycle.

Downcity hooks have three layers: Action -> Service -> Global. Execution order runs from outer to inner layers:

global.before
  -> service.before
    -> action.before
      -> action.run()  <- core logic
    -> action.after    <- billing
  -> service.after
-> global.after

Action-level hooks

Each Action has its own hooks, which makes them a good fit for studio-specific logic:

const zh2en = svc.action("zh2en", async (ctx) => {
  return await translate(ctx.input.text, "en");
});

// Check balance before the call
zh2en.before(async (ctx) => {
  const bal = await balance.get(ctx.user!.user_id);
  if (bal <= 0) throw new Error("Insufficient balance");
});

// Deduct balance after the call (ctx.output is available)
zh2en.after(async (ctx) => {
  await deductBalance(ctx.user!.user_id, 10);
  await logUsage(ctx);
});

Service-level hooks

All Actions under the same Service share them:

svc.hook.before(async (ctx) => {
  console.log(`${ctx.user?.id} -> ${ctx.service?.id}.${ctx.action?.id}`);
});

svc.hook.after(async (ctx) => {
  // aggregate metrics, unified logs
});

Comparison

Action hookService hook
GranularitySingle actionAll actions in the service
Use casesBalance checks, billing, per-action logsShared validation, aggregate metrics
Registrationaction.before(fn)svc.hook.before(fn)

Key fields inside ctx

interface Context {
  input: Record<string, unknown>    // request payload
  output?: unknown                  // Action result, available in after
  user?: { user_id, metadata }      // current user
  studio?: { studio_id, status }  // current studio
  service?: { id, name }            // current Service
  action?: { id }                   // current Action
}