Hooks and Service Mounting
The three hook layers, service mounting, and why they are the shared extension points of City.
A lot of shared capability in Downcity is not created by copying logic into every action. It enters City through hooks and services.
What each concept solves
The easiest split is:
- hooks: insert rules into an existing call lifecycle
- services: add a full reusable capability set into City
That is why auth, usage, and payment do not become separate systems. They become extension points inside the same Federation.
When to think hook first
- you want logic before or after existing calls
- the logic is cross-cutting and should not live in every action
- the capability is local to your project and does not need to be its own package
Typical examples:
- quotas
- logging
- risk checks
- charging after a successful call
When to think service first
- the capability should be reused across many products
- it is more than before or after logic and needs its own service or tables
- you want product-side callers to reach it through a stable service boundary
Typical examples:
@downcity/services@downcity/services@downcity/services
First understand the three hook positions
global
-> service
-> actionTheir meanings are:
- global: shared by all services and actions
- service: shared by all actions in one service
- action: only affects one concrete action
Common service mounting example
import { Federation } from "@downcity/city";
import {
AccountsService,
BalanceService,
PaymentService,
googleAccountsProvider,
stripePaymentProvider,
UsageService,
} from "@downcity/services";
const base = new Federation({ db });
const balance = new BalanceService();
base.use(new AccountsService({
token_ttl: "7d",
providers: [
googleAccountsProvider(),
],
}));
base.use(balance);
base.use(new PaymentService({
readTopup: async (topup_id) => await balance.readTopup(topup_id),
finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
providers: [
stripePaymentProvider({
secret_key: process.env.STRIPE_SECRET_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
}),
],
}));
base.use(new UsageService({
record_errors: true,
}));In this setup, AccountsService owns the accounts HTTP surface. Mounting it once exposes /providers, me, admin reads, the better-auth passthrough route, and the OAuth callback route. Concrete login methods appear only when the matching provider is mounted and configured.
How product-side callers reach services
Once mounted, services still look like normal services from the product side:
const session = await guest.service("accounts").action("login").invoke({
email: "[email protected]",
password: "password123",
city_id: "city_demo",
});
const usage = await admin.service("usage").get("summary");
const methods = await guest.service("payment").get("methods");
const checkout = await user.service("payment").action("checkout/create").invoke({
method_id: "stripe",
topup_id: "topup_demo",
});That means payment is both the discovery entry and the execution entry; Stripe is selected as a provider.
Relationship between hooks, services, and services
A useful chain is:
service / action
-> hooks add lifecycle rules
-> services add reusable services, tables, and management capability
-> clients still call everything through /v1/*Common API surface
base.use(service)guest.service("accounts")...guest.service("payment").get("methods")user.service("usage")...admin.service("payment").get("payments")
Common misunderstandings
A service is not just another npm package
It often represents a whole boundary of services, tables, admin surfaces, and optional hook behavior.
Hooks are not a place to hide full business features
Hooks are best for cross-cutting governance logic, not for turning every feature into middleware.
Services do not replace City
Services add capability into the Federation. The product-side calling surface is still @downcity/city.
Read next
- For unified product-side service calls, read @downcity/city
- For auth capability, read @downcity/services
- For usage recording, read @downcity/services
- For one-time Stripe topups, read @downcity/services