Local API

The Local API lets you query and mutate data programmatically from your TypeScript code — inside hooks, access rules, custom routes, cron jobs, or anywhere you have access to the app instance. These methods bypass HTTP and go straight to the database adapter.

Methods

find(collection, query?)

Query records with filtering, sorting, and pagination. Returns a paginated result.

const result = await app.find("todos", {
  where: { completed: { eq: false } },
  sort: "-createdAt",
  limit: 10,
  page: 1,
});

// result.docs — array of records
// result.totalDocs — total matching records
// result.totalPages — total pages
// result.hasNextPage — boolean
// result.hasPrevPage — boolean

findById(collection, id, query?)

Fetch a single record by ID. Returns null if not found.

const todo = await app.findById("todos", "abc-123");
if (!todo) {
  console.log("Not found");
  return;
}
console.log(todo.title);

create(collection, data)

Create a new record. Returns the created record (with id, createdAt, updatedAt).

const todo = await app.create("todos", {
  title: "Buy groceries",
  completed: false,
  author: ctx.user.id,
});

update(collection, id, data)

Update a record by ID. Returns the updated record.

const updated = await app.update("todos", "abc-123", {
  completed: true,
});

delete(collection, id)

Delete a record by ID.

await app.delete("todos", "abc-123");

count(collection, query?)

Count records matching a query.

const total = await app.count("todos");
const completed = await app.count("todos", {
  where: { completed: { eq: true } },
});

Query Structure

interface QueryArgs {
  where?: WhereClause;
  sort?: string;
  limit?: number;
  page?: number;
  depth?: number;  // Relationship populate depth
}

Where Clauses

Each field in a where clause supports filter operators:

await app.find("todos", {
  where: {
    title: { eq: "Buy groceries" },           // equals
    completed: { neq: true },                  // not equals
    priority: { in: ["high", "urgent"] },     // in array
    status: { nin: ["archived", "deleted"] }, // not in array
    createdAt: { gt: "2026-01-01" },          // greater than
    createdAt: { gte: "2026-01-01" },         // greater than or equal
    createdAt: { lt: "2026-12-31" },          // less than
    createdAt: { lte: "2026-12-31" },         // less than or equal
    title: { like: "%grocer%" },              // LIKE pattern
    title: { nlike: "%test%" },               // NOT LIKE
  },
});

Combining with AND/OR

await app.find("todos", {
  where: {
    OR: [
      { completed: { eq: false } },
      { priority: { eq: "urgent" } },
    ],
  },
});

Sorting

Prefix with - for descending order:

await app.find("todos", { sort: "-createdAt" });   // newest first
await app.find("todos", { sort: "priority" });      // ascending
await app.find("todos", { sort: "-priority,createdAt" }); // multiple fields

Pagination

await app.find("todos", { limit: 20, page: 1 });

Relationship Populate (depth)

Use depth to populate relationship fields with the full referenced record:

// depth: 0 (default) — author is just the ID string
const todos = await app.find("todos", { depth: 0 });
// todos.docs[0].author → "user-uuid-string"

// depth: 1 — author is populated with the full user object
const populated = await app.find("todos", { depth: 1 });
// populated.docs[0].author → { id: "user-uuid", name: "John", ... }

Using Inside Hooks

app.hooks("todos", {
  beforeCreate: async (ctx) => {
    // Check if the user already has a todo with the same title
    const existing = await ctx.radiant.find("todos", {
      where: {
        author: { eq: ctx.user!.id },
        title: { eq: ctx.data.title },
      },
      limit: 1,
    });
    if (existing.docs.length > 0) {
      throw new Error("You already have a todo with this title");
    }
    return ctx.data;
  },
});

Using Inside Custom Routes

app.router.get("/dashboard/:userId", async (ctx) => {
  const [todos, posts] = await Promise.all([
    ctx.radiant.find("todos", {
      where: { author: { eq: ctx.params.userId } },
      limit: 10,
    }),
    ctx.radiant.find("posts", {
      where: { author: { eq: ctx.params.userId } },
      limit: 5,
    }),
  ]);

  return {
    todos: todos.docs,
    posts: posts.docs,
  };
});

Using Inside Access Rules

app.access("documents", {
  read: async (ctx) => {
    if (!ctx.user) return false;
    const membership = await ctx.radiant.find("memberships", {
      where: {
        userId: { eq: ctx.user.id },
        orgId: { eq: ctx.data.orgId },
      },
      limit: 1,
    });
    return membership.docs.length > 0;
  },
});

Audit Logging

When security.audit.enabled is true in your DSL, the Local API automatically logs all create, update, and delete operations to the radiant_audit_log collection with HMAC-signed entries.

  • REST API — The auto-generated HTTP endpoints that mirror these methods
  • Hooks — Where you'll most commonly use the Local API
  • Access Control — Using the Local API inside access rules