syncore-functions
Syncore Functions
Use this skill when writing or reviewing syncore/functions/**/*.ts files. The
focus is preserving typed DX from function definitions through generated
references and client APIs.
Documentation Sources
Read these repo-local references first:
packages/core/src/runtime/functions.tspackages/core/src/runtime/runtime.tspackages/schema/src/validators.tspackages/core/AGENTS.mdREADME.mddocs/architecture.mdexamples/electron/syncore/functions/entries.tsexamples/expo/syncore/functions/notes.tsexamples/next-pwa/syncore/functions/bookmarks.tsexamples/sveltekit/syncore/functions/habits.ts
Instructions
Function Types Overview
| Type | Use For | Database Access | External IO |
|---|---|---|---|
query |
Reading reactive state | Read-only | No |
mutation |
Transactional writes | Read/Write | No |
action |
Side effects and integrations | Via runQuery and runMutation |
Yes |
Use Generated Server Helpers In Function Files
Inside syncore/functions/*.ts, import from ../_generated/server:
import { mutation, query, v } from "../_generated/server";
This keeps app code aligned with current codegen output and shared validator types.
Prefer Strong Validators
Use the most specific validators you can. For document ids, prefer table-aware ids over plain strings:
args: {
id: v.id("tasks"),
done: v.boolean()
}
That keeps intent clear and improves downstream typing.
Queries
Queries are reactive and should read only:
import { query, v } from "../_generated/server";
export const list = query({
args: {},
returns: v.array(
v.object({
_id: v.string(),
text: v.string(),
done: v.boolean()
})
),
handler: async (ctx) =>
ctx.db.query("tasks").withIndex("by_done").order("desc").collect()
});
The current query builder surface includes:
withIndex(...)withSearchIndex(...)filter(...)collect()take(count)first()unique()paginate({ cursor, numItems })
Use indexes and search indexes from schema before depending on those query paths in functions.
Mutations
Mutations own writes and can schedule follow-up work:
import { mutation, v } from "../_generated/server";
export const toggleDone = mutation({
args: {
id: v.id("tasks"),
done: v.boolean()
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch("tasks", args.id, { done: args.done });
return null;
}
});
Mutations have ctx.db, ctx.storage, ctx.scheduler, ctx.runQuery,
ctx.runMutation, and ctx.runAction.
Actions
Actions are the place for side effects or other non-database integrations:
import { action, v } from "../_generated/server";
import { api } from "../_generated/api";
export const exportTasks = action({
args: {},
returns: v.number(),
handler: async (ctx) => {
const tasks = await ctx.runQuery(api.tasks.list);
await fetch("https://example.invalid/export", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(tasks)
});
return tasks.length;
}
});
Actions also have access to ctx.storage and ctx.scheduler.
Typed References
There are two main reference flows:
- generated client references via
syncore/_generated/api - direct references via
createFunctionReferenceorcreateFunctionReferenceFor
Generated references are preferred in app code. Direct references are useful for scheduler jobs, browser-ESM samples, and low-level runtime tests.
import { createFunctionReference, mutation, v } from "../_generated/server";
export const scheduleCreate = mutation({
args: { body: v.string(), delayMs: v.number() },
handler: async (ctx, args) =>
ctx.scheduler.runAfter(
args.delayMs,
createFunctionReference("mutation", "notes/createFromScheduler"),
{ body: args.body, pinned: false },
{ type: "catch_up" }
)
});
Empty Args Ergonomics Matter
Syncore intentionally supports optional call signatures for empty-object args. Preserve these patterns:
export const list = query({
args: {},
handler: async (ctx) => ctx.db.query("tasks").collect()
});
That enables client usage like:
const tasks = useQuery(api.tasks.list) ?? [];
Do not introduce type changes that force useless {} arguments at every
callsite unless the public API is intentionally changing.
Examples
Complete Function File
import {
createFunctionReference,
mutation,
query,
v
} from "../_generated/server";
export const listPinned = query({
args: {},
handler: async (ctx) =>
ctx.db.query("notes").withIndex("by_pinned").order("asc").collect()
});
export const searchNotes = query({
args: { term: v.string() },
handler: async (ctx, args) =>
ctx.db
.query("notes")
.withSearchIndex("search_body", (search) =>
search.search("body", args.term).eq("pinned", false)
)
.take(20)
});
export const create = mutation({
args: { body: v.string() },
handler: async (ctx, args) =>
ctx.db.insert("notes", { body: args.body, pinned: false })
});
export const createFromScheduler = mutation({
args: { body: v.string(), pinned: v.boolean() },
handler: async (ctx, args) =>
ctx.db.insert("notes", { body: args.body, pinned: args.pinned })
});
export const scheduleCreateSkip = mutation({
args: { body: v.string(), delayMs: v.number() },
handler: async (ctx, args) =>
ctx.scheduler.runAfter(
args.delayMs,
createFunctionReference("mutation", "notes/createFromScheduler"),
{ body: args.body, pinned: false },
{ type: "skip" }
)
});
Best Practices
- Use
queryfor reads,mutationfor writes, andactionfor side effects - Import helpers from
../_generated/serverinside function files - Prefer
v.id("table")over plainv.string()for document ids - Add
returnsvalidators where explicit shape matters to callers - Preserve optional args ergonomics for empty-object validators
- Prefer generated references in app code and explicit references in low-level runtime flows
- Keep type changes aligned across core, codegen, React, and adapters
Common Pitfalls
- Using
actionfor ordinary database writes that belong in mutations - Breaking
useQuery(api.foo.bar)inference by widening reference types - Editing generated API references instead of source function definitions
- Forgetting that scheduler APIs accept typed function references and optional misfire policies
- Reaching for plain strings where
v.id("table")better expresses intent
References
packages/core/src/runtime/functions.tspackages/core/src/runtime/runtime.tspackages/schema/src/validators.tspackages/core/AGENTS.mdexamples/electron/syncore/functions/entries.tsexamples/expo/syncore/functions/notes.tsexamples/next-pwa/syncore/functions/bookmarks.ts