Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Where to put your code

Find the right home for functions, tools, agents, events, jobs, databases, storage, schemas, routes, and shared code.

Keep files with the service that owns their behavior, regardless of how callers reach them. For example, an order lookup belongs to Orders whether it is reached from HTTP, another function, or an AI model. Its implementation stays in src/services/orders/functions/; the entry points reference it.

The placement map

The paths below are patterns, not files you must create in every domain. Replace orders with the owner.

What you are adding Where it belongs Continue learning
Public domain boundary src/services/orders/service.ts Define a service
Business operation src/services/orders/functions/create-order.function.ts Functions
Event contract src/services/orders/events/order-created.event.ts Events: getting started
Event subscriber functions/*.function.ts in the receiving domain, such as src/services/receipts/functions/order-receipt.function.ts Consume an event
Task and its job src/services/receipts/tasks/send-receipt.task.ts and src/services/receipts/jobs/send-receipt.job.ts Jobs: getting started and schedules
AI tool src/services/orders/tools/lookup-order.tool.ts AI: getting started, then tools
AI agent src/services/orders/agents/order-support.agent.ts AI: getting started, then your first agent
Cache src/services/orders/cache/prices.cache.ts Caching: getting started
Object bucket src/services/assets/buckets/assets.bucket.ts in the domain that owns the files Storage: getting started
Database service src/services/database/service.ts using defineDrizzleService Database: getting started
Database table src/services/database/schema/, exported through its index.ts Define the schema
Custom data-model method src/services/database/models/*.model.ts, registered in the database service Custom models
Database migration Tracked SQL/metadata under src/services/database/migrations; runner owned by installed tooling Migrations
Domain schema Beside its sole consumer, or a domain module such as src/services/orders/schemas.ts when shared locally Schemas
Declared domain error src/services/orders/errors/order-not-found.error.ts Errors
Prompt, constant, or helper Beside its consumer or in a focused module within the same domain Agent instructions, domain boundaries
URL and HTTP method src/routes/orders/route.ts Services and routes
HTTP middleware or input transform src/routes/middleware/*.middleware.ts or src/routes/transforms/ Middleware, request mapping
Environment contract src/platform/env.ts Environment
Provider and server configuration relkit.config.ts, importing infrastructure support from Platform Integrations, application configuration
Function and application tests tests/unit/ and tests/integration/ Testing

Default-export each ordinary descriptor from its own file, following its capability’s guide. Route files are different: they export named HTTP methods. Plain helper modules are ordinary TypeScript; they do not need a RELKIT descriptor or their own service.

One selected service root

New applications place boundaries at src/services/<name>/service.ts. Existing flat applications keep src/<name>/service.ts, and relkit add preserves the detected layout. Choose one root for the whole application, including Database and Auth. A directory alone, an empty services/ directory, or a folder containing only helpers does not select a layout. Discovery uses authored service and descriptor evidence.

Each service is an immediate child of the selected root. Every descriptor belongs beneath exactly one boundary. Nested boundaries, mixed flat/grouped services, missing boundaries, and an aggregate src/services/service.ts are structural errors. A source-only move keeps logical IDs, URLs, exposure, resource ownership and policies stable. Use the optional migration guide for coordinated moves and rollback.

Imports follow the boundary

Service implementation files can import their own files, Platform, and another service’s public service.ts boundary. Routes can import service boundaries and Platform. Platform can import only Platform within application source; relkit.config.ts can import Platform fragments. Application code cannot depend on tests, migrations, scripts or other tooling. Tooling can import the application for testing and operational work.

The compiler follows helper modules, re-exports, type-only edges and literal dynamic imports using the project’s effective TypeScript aliases. A barrel cannot make a private cross-service import public. Computed dynamic imports and unresolved dynamic targets produce analysis-limit warnings; they are not proof that the target respects a boundary.

Keep prompt text, JSON assets, schemas and local helper modules beside their consumer. Assets move with their service but do not become independently discovered descriptors.

Functions own behavior; dependencies grant clients

A function is where validated business input becomes a result. Keep HTTP parsing in routes and model instructions in agents. The function declares the provider-backed resources it uses; putting a cache or bucket in the same folder does not grant access automatically.

/** Prices validated order input through the declared cache, records request tracing and derives its receipt key. */
import { defineFunction } from "@relkit/app/functions";
import prices from "../cache/prices.cache.js";
import { receiptObjectName } from "../../../platform/receipt-object.js";
import { createOrderOutput, orderInput } from "../../../platform/schemas.js";

const createOrder = defineFunction({
  // RELKIT validates these schemas before the handler runs.
  input: orderInput,
  output: createOrderOutput,
  dependencies: {
    cache: { prices },
  },
  timeoutMs: 10_000,
  concurrency: 100,
  handler: async (input, context) => {
    // Dependencies are accessed through the checked execution context.
    const unitPrice = await context.trace.span(
      "pricing.calculate",
      { attributes: { "pricing.strategy": "catalog" } },
      () => context.cache.prices.getOrSet({ sku: input.sku }, async () => 1_000),
    );
    context.trace.event("pricing.calculated", { "pricing.unit_cents": unitPrice });
    context.trace.setAttributes({ "pricing.quantity": input.quantity });
    const totalCents = unitPrice * input.quantity;
    return {
      orderId: input.orderId,
      receiptKey: receiptObjectName(input.orderId),
      totalCents,
    };
  },
});

export default createOrder;

Here, dependencies.cache supplies context.cache.prices. Event publishers separately declare publishes before using context.events; see publish an event. Neither permission is configured in service.ts. The price cache is an Orders implementation detail, while createOrder can be a public operation.

Use functions for the handler and validation model, context for execution resources, and your first cache for a complete resource-backed example.

Tools and agents stay with their business domain

Put an order lookup tool in Orders, not in a separate application-wide AI domain just because a model calls it. A tool points at a function with defineTool({ target: ... }) or adapts that function with .asTool(). The schemas and business behavior stay with the function. Start with defining a tool or turning a function into a tool.

An agent’s agents/ file owns its instructions, schemas, model, tool allowlist, and execution limits. Store long instructions in a local prompt module if that makes them easier to maintain. Plain text does not need a new service. See defining an agent.

Native execution requires installing @relkit/langchain and its selected SDKs and configuring the required runtime profiles. A folder or generic agent contract does not select that engine.

To let a browser run an agent, expose its client contract and select it with the generated useAgent hook. Every run, observation, control, and continuation supplies the application’s stable business thread ID. Agents and tools remain compiler-discovered domain files; do not add an agents or tools map to defineService. A tool in another domain can target a public function from the owning service; it should not import that domain’s private tool file.

“Internal to the domain” does not mean “hidden from MCP.” When the endpoint is explicitly enabled, tools are eligible by default unless disabled. Complete the separate MCP SDK/setup and review tool approvals before exposing them.

Events and jobs belong to the work’s owner

Put the fact in the producing domain’s events/ directory and the reaction in the receiving domain’s functions/ directory. For example, Orders defines orders.created, while Receipts defines the defineEventFunction that reacts to it. The subscriber stays out of service.functions: it runs through event delivery or replay, not direct invocation.

A task and job belong to the domain responsible for completing the durable work. The task stays in that domain’s tasks/ directory and the job owns its public name, admission, and schedules. A schedule is an option on the job, not a separate service or a reason to move the task. Follow your first event, jobs, and schedules for the complete workflows.

Resources do not go in the service member list

Caches and buckets live under their owning domains. Functions declare dependencies.cache or dependencies.buckets, and relkit.config.ts supplies provider bindings for the selected profiles. Changing the provider does not require moving the domain’s business code.

If another domain needs access, expose an operation on the owner—such as uploading or reading a file— instead of publishing the raw cache or bucket as a service member. See the storage tutorial for a bucket, function, service, and route working together.

Database and authentication integrations have specialized boundaries: src/services/database/service.ts uses defineDrizzleService, with table schemas beneath that domain; src/services/auth/service.ts uses defineBetterAuthService. They are not database or auth options on defineService. Start with Database for setup, schemas, queries, and model customization. The compiler supplies context.database; functions do not add a dependencies.database entry. Ordinary business domains still expose operations through defineService. See the Database and Auth guidance when replacing older adapters, or Integrate Better Auth to share the current database with authentication. Continue with Auth for service setup, sessions, route protection, clients, and authentication customization.

Shared code is not a shortcut around ownership

Keep domain-specific schemas, constants, errors, and helpers with their owner. Other domains consume public service operations instead of importing those private modules. A public function’s declared errors are part of its checked contract; you do not add an errors member map to the service.

Use src/platform/ for genuinely shared, domain-independent support, such as the environment contract or common schema primitives. Platform cannot import domains or routes. If a “shared” helper knows how Orders calculates a total, it belongs to Orders.

Tests live outside src/ in the generated layout. Unit tests can import the specific function they exercise; integration tests verify the application’s routes and runtime behavior. Do not add a domain service just to hold tests.

Let the compiler check the organization

Run these commands from your generated application’s root after adding or moving artifacts:

bun run check
bun run typecheck

Expect a successful graph check and type check. Fix missing services, invalid IDs, unresolved targets, and boundary errors before continuing. Do not register every file manually in relkit.config.ts or edit .relkit/generated; the compiler derives the model from source.

Naming and folder-placement suggestions remain nonfatal convention warnings. They are distinct from ownership, missing-boundary, mixed-layout and forbidden-import errors.

Reference and source