Generated code

What @lunora/codegen emits from your schema.ts, and how api / Id / Doc keep server and client in sync.

Last updated:

@lunora/codegen reads your schema.ts (and the functions it discovers) and emits a _generated/ directory next to it. You never edit these files; they are derived artifacts, regenerated on every save. What they buy you is end-to-end type safety: a query's argument and return types are inferred on the server and handed to the client through api, so a rename or a wrong arg is a compile error, not a runtime surprise.

Three files are emitted from schema.ts:

FileWhat it gives you
api.tsthe typed api.* (public) and internal.* (server-only) function registries
server.tsthe project-typed server helpers — query/mutation/action, v, typed ctx
dataModel.tsthe data-model types — Id<"table">, Doc<"table">, and insert/document shapes

api.ts — function references

api is a typed registry of your public functions; internal is the same registry for functions you mark internal. Each entry is a FunctionReference carrying the function's kind, argument type, and return type. That's what lets a client call hooks with full inference:

import { useQuery } from "@lunora/react";
import { api } from "@/lunora/_generated/api";

// args are checked against the query's input validator; the result is typed.
const messages = useQuery(api.messages.list, { channelId });

api and internal are the same proxy at runtime; visibility is enforced server-side at dispatch, not in the reference. Splitting the types is what keeps internal functions off the client-facing api surface, so you can't accidentally call one from the browser. Internal functions are reachable only server-side via ctx.runQuery / ctx.runMutation / ctx.runAction, passing an internal.* reference.

server.ts — typed helpers

server.ts re-exports the procedure builders and v validators bound to your schema, so ctx.db knows your tables. Author functions by importing from the generated server (not the base package); then ctx.db.query("messages"), ctx.db.insert("messages", …), and ctx.db.get(id) are all typed against your real schema with no casts.

dataModel.tsId and Doc

Two types you reach for constantly:

  • Id<"table"> — a branded string id. It carries its table name in the type, so ctx.db.get(id) resolves to the right document type and you can't pass a users id where a messages id is expected.
  • Doc<"table"> — the full stored document shape for a table, including the built-in _id and _creationTime.
import type { Doc, Id } from "@/lunora/_generated/dataModel";

function format(message: Doc<"messages">): string {
    const author: Id<"users"> = message.userId;
    return `${author}: ${message.text}`;
}

Importing the generated code

The CLI scaffolds your functions under lunora/, with _generated/ alongside. Import api/internal from _generated/api, data-model types from _generated/dataModel, and the typed builders from _generated/server. Most projects alias the directory (e.g. @/lunora/_generated/*).

lunora/* vs @lunora/* imports

Codegen detects whether your project depends on the unscoped lunora umbrella package or on the granular @lunora/* packages, by inspecting the project's declared dependencies, and emits matching imports in _generated/*:

  • depend on lunorash → the generated files import from lunorash/server, lunorash/server/types, lunorash/client, etc.
  • depend on @lunora/server, @lunora/client, … → they import from @lunora/server, @lunora/server/types, @lunora/client, etc.

This is opt-in and fully backward-compatible: switching to the umbrella changes only the import specifiers in the generated code, never your function code.

Regenerating

You rarely run codegen by hand. The Vite plugin (@lunora/vite) regenerates _generated/* on every change to schema.ts or your functions during lunora dev, and codegen also runs on deploy. The Advisors static lints run as part of the same pass, so schema problems show up the moment you save.

For CI, scripts, or a manual refresh, run it directly:

lunora codegen

See also