Execution Context & Services
Resolve request context, inject databases and users, and isolate in-process service calls.
Execution context provides request metadata, database handles, logging, and user authentication state to actions and lifecycle hooks.
Minimal Example
Configure a service context resolver to parse authentication headers and inject user identity into actions:
import { defineService, t } from "@brickkit/core";
// Define custom session shape
interface BillingUser {
id: string;
plan: "free" | "pro";
}
export const billingService = defineService("billing", {
context: async (baseCtx): Promise<{ user: BillingUser | null }> => {
const auth = baseCtx.request.headers.get("authorization");
if (!auth) return { user: null };
// Example token validation
const user =
auth === "Bearer pro-token"
? { id: "usr_42", plan: "pro" as const }
: { id: "usr_10", plan: "free" as const };
return { user };
},
});
export const getSubscription = billingService.action({
name: "getSubscription",
output: t.Object({ plan: t.String() }),
execute: ({ ctx, error }) => {
// Error helpers throw; `return` lets TypeScript narrow `ctx.user`
if (!ctx.user) return error.UNAUTHORIZED("Sign in required");
return { plan: ctx.user.plan };
},
});BaseContext Properties
Every action invocation and service resolver receives a BaseContext containing HTTP and runtime telemetry primitives:
| Field | Type | Description |
|---|---|---|
request | Request | Web-standard Request for the active HTTP call, or a placeholder http://localhost request in-process. |
db | TDb | Database instance from the owning service's handle. |
logger | Logger | Logger with .info(), .warn(), .error(), and .debug(). |
traceId | string | Trace ID. Over HTTP, taken from the x-trace-id header when present (e.g., tr_k2l9p0). |
requestId | string | Request ID. Over HTTP, taken from the x-request-id header when present (e.g., req_k2l9p0). |
services | Record<string, any> | Reserved. Brick passes an empty object; use createServiceProxy to call other services. |
metadata | Record<string, unknown> | Over HTTP, { headers } with the request headers; an empty object in-process. |
Over HTTP, Brick echoes traceId and requestId back as x-trace-id and x-request-id response headers.
Service Context Resolvers
Brick passes BaseContext to the service's context resolver on every HTTP request and every in-process call:
export const projectsService = defineService("projects", {
context: async (baseCtx) => {
const tenantId = baseCtx.request.headers.get("x-tenant-id") ?? "default";
baseCtx.logger.info(`Resolving context for tenant: ${tenantId}`);
return {
tenantId,
resolvedAt: Date.now(),
};
},
});The returned object is merged with BaseContext to form ActionContext. Inside execute({ ctx }), TypeScript infers both BaseContext properties (like ctx.db and ctx.logger) and the resolved properties (like ctx.tenantId).
Context Isolation Across In-Process Service Calls
When calling another service in-process using createServiceProxy, Brick enforces strict context isolation:
import { createServiceProxy, defineService } from "@brickkit/core";
export const notificationsService = defineService("notifications");
// The type parameter is the shape you call; Brick doesn't infer it
type UsersApi = {
getUser: (input: {
id: string;
}) => Promise<{ id: string; name: string; email: string }>;
};
// Proxy to the "users" service, looked up by name in the global registry.
// `from` identifies the calling service, so `users` actions see `ctx.caller === "notifications"`.
const users = createServiceProxy<UsersApi>("users", { from: "notifications" });
export const sendWelcome = notificationsService.action({
name: "sendWelcome",
execute: async ({ ctx }) => {
// Proxy methods take (input, callCtx?) and run the action in-process
const user = await users.getUser({ id: "usr_1" });
ctx.logger.info(`Sending welcome email to ${user.email}`);
return { sent: true };
},
});Each proxy method takes the action's input as its first argument and an optional call context as its second. Only in-process (monolith) dispatch is implemented; createServiceProxy(name, { isMonolith: false }) throws when called.
Security Boundaries
What the receiving service sees:
- Own context and database: The receiving service runs its own
contextresolver and uses its own database. - Framework fields are dropped:
db,request,logger,services, andmetadatain the call context are discarded before reaching the receiving service. - Envelope fields pass through: Only
caller,user,traceId,requestId, andtenantIdare forwarded. The resolver receives these fields and its returned values always take precedence. Caller custom fields such assessionandisOwnerare discarded for direct action calls and proxy calls. TreattenantIdas a claim to validate, not proof of access. ctx.caller: The proxy setsctx.callertofrom— the name you passed when creating the proxy,createServiceProxy<UsersApi>("users", { from: "notifications" })— never the target service's name. Whenfromis provided, anycallerin the proxy'soptions.contextor the per-call context is ignored, so it cannot be spoofed. Withoutfrom, an explicitly suppliedcallerstill passes through; otherwisecalleris left unset.
Internal calls without cookies
A proxy call receives a placeholder request without browser cookies. A resolver that supports internal callers should validate envelope.user and derive its own session and helpers. For example, accept a user ID only after checking it with your identity store and caller trust policy:
const accounts = defineService("accounts", {
context: async (envelope) => {
const claim = envelope.user;
const userId =
typeof claim === "object" &&
claim !== null &&
"id" in claim &&
typeof claim.id === "string"
? claim.id
: undefined;
// Application functions validate the caller and load an allowed identity.
const user =
userId && isTrustedCaller(envelope.caller)
? await loadAllowedUser(userId, envelope.tenantId)
: null;
return {
user,
session: null,
isOwner: (ownerId: string) => user?.id === ownerId,
};
},
});For HTTP calls, continue resolving identity from the actual request's authentication headers or cookies. Passing { user } directly cannot replace a resolver's returned user, even when the resolver returns null. Tests should provide a request or configure a resolver that explicitly validates the internal envelope.