syncore-best-practices
Syncore Best Practices
Build Syncore applications around the current source of truth in this repository: local runtimes, generated typed APIs, thin app bindings, and platform-specific bootstrap layers.
Documentation Sources
Read these first:
README.mddocs/architecture.mddocs/development.mddocs/guides/syncore-vs-convex.mdexamples/AGENTS.mdpackages/core/AGENTS.mdpackages/cli/AGENTS.mdpackages/react/AGENTS.md
Instructions
Project Shape
The intended app layout is:
syncore/
schema.ts
functions/
tasks.ts
migrations/
_generated/
syncore.config.ts
Keep concerns separated:
syncore/schema.tsdefines tables, indexes, and search indexessyncore/functions/**/*.tsdefinesquery,mutation, andactionsyncore/_generated/*is CLI output- app code imports typed references from
syncore/_generated/api - backend files import helpers from
syncore/_generated/server
Prefer The Current Happy Path
Inside an app, the main local loop is:
npx syncorejs dev
syncorejs dev can scaffold a missing Syncore project, keep generated files
fresh, check schema drift, apply local migrations, and run the local hub.
Use npx syncorejs init --template <template> when you want explicit
scaffolding instead of auto-detection.
Generated Files Are Outputs
Treat these as generated artifacts:
syncore/_generated/api.tssyncore/_generated/functions.tssyncore/_generated/server.ts
Do not hand-edit them. If they are wrong, fix:
- source functions
- schema validators
- CLI codegen
- runtime, React, Svelte, or adapter inference
Prefer Public Entry Points In App Code
User-facing code should normally import from public syncorejs/* entrypoints
such as:
syncorejssyncorejs/reactsyncorejs/browsersyncorejs/browser/reactsyncorejs/nodesyncorejs/node/ipcsyncorejs/node/ipc/reactsyncorejs/exposyncorejs/expo/reactsyncorejs/nextsyncorejs/next/configsyncorejs/svelte
Reach for @syncore/* package names mainly when editing monorepo internals.
Optimize For DX At The Type Source
Syncore's DX depends on types flowing from source definitions into generated references and hooks.
Prefer this:
const tasks = useQuery(api.tasks.list) ?? [];
const createTask = useMutation(api.tasks.create);
Be suspicious of this:
const tasks = useQuery<{ _id: string; text: string }[]>(api.tasks.list) ?? [];
const createTask = useMutation<string>(api.tasks.create);
If manual generics become necessary in app code, investigate core, codegen, or adapter typing before normalizing the workaround.
Let Examples Stay Small
Examples are integration fixtures, not product apps.
- keep them minimal
- keep them representative of public APIs
- do not hide DX bugs with one-off local annotations when the real fix belongs in shared packages
Use Built-In Local Data Workflows
For local sample data, prefer CLI workflows over ad hoc SQL or edited generated artifacts:
npx syncorejs import --table <table> <file>for explicit JSONL importsnpx syncorejs seed --table <table>for conventional seed files undersyncore/seednpx syncorejs seed --table <table> --file <file>when the fixture lives elsewhere
Validate Cross-Package Changes Together
DX changes often span:
packages/corepackages/schemapackages/clipackages/reactpackages/svelte- platform adapters
- examples
When you change a type boundary, verify all affected layers together.
If you intentionally change public exports, refresh the API Extractor baselines
with bun run api:update and confirm bun run api:check passes.
Examples
Recommended Function File
import { mutation, query, v } from "../_generated/server";
export const list = query({
args: {},
handler: async (ctx) =>
ctx.db.query("tasks").withIndex("by_done").order("desc").collect()
});
export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) =>
ctx.db.insert("tasks", { text: args.text, done: false })
});
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;
}
});
Recommended React Usage
import { useMutation, useQuery } from "syncorejs/react";
import { api } from "../syncore/_generated/api";
function Tasks() {
const tasks = useQuery(api.tasks.list) ?? [];
const createTask = useMutation(api.tasks.create);
return (
<button onClick={() => void createTask({ text: "Work offline" })}>
Add task
</button>
);
}
Best Practices
- Keep user code under
syncore/and generated code undersyncore/_generated/ - Import server helpers from
../_generated/serverinside function files - Import typed references from
syncore/_generated/apiin app code - Prefer fixing inference in shared packages rather than adding manual generics in examples
- Treat
npx syncorejs devas the main local development loop - Use examples as fixtures to confirm intended DX, not as a place to patch over shared regressions
Common Pitfalls
- Editing generated files directly instead of fixing codegen or source definitions
- Solving type regressions with app-level casts instead of shared fixes
- Forgetting that examples are smoke fixtures and must stay deterministic
- Importing internal
@syncore/*packages in app docs when the publicsyncorejs/*surface is the intended API - Assuming Convex conventions apply unchanged when Syncore's local runtime differs
References
README.mddocs/development.mddocs/guides/syncore-vs-convex.mdexamples/AGENTS.mdpackages/core/AGENTS.mdpackages/cli/AGENTS.mdpackages/react/AGENTS.md