Functions and Bindings
Define backend functions in Go and call them from the frontend through generated TypeScript bindings.
Functions and Bindings
Gonvex apps define backend functions in Go. The CLI reads those registrations and generates TypeScript references for the frontend.
Register Functions
package backend
import "github.com/gonvex/gonvex/pkg/gonvex"
func Register(app *gonvex.App) {
app.Query(
"tasks.list",
ListTasks,
gonvex.Reads("tasks").
Columns("id", "title", "status", "updated_at").
Filters("status").
OrdersBy("updated_at"),
gonvex.ShareByPermissions(),
)
app.Mutation("tasks.create", CreateTask, gonvex.Writes("tasks"))
app.Action("tasks.import", ImportTasks)
app.HTTP("/webhooks/stripe", StripeWebhook)
app.PublicHTTP("/health/public", PublicHealth)
app.InternalMutation("tasks.expire", ExpireTasks, gonvex.Writes("tasks"))
app.LiveGrid("tasks.grid", TasksGrid)
app.Sync(
"tasks.recent",
RecentTasks,
gonvex.SyncTable("tasks").
Columns("id", "title", "status", "updated_at").
OrderBy("updated_at", "desc").
Progressive().
Budget(500, 8388608),
)
}
Dependency options are backward compatible. Registrations without them use
conservative compatibility invalidation. ShareByPermissions is intentionally
opt-in: use it only when the result depends on tenant, arguments, and the
permission set, but not otherwise on ctx.User.
Function Kinds
Query
Read data. Can be subscribed to from the frontend.
Mutation
Write data inside a transaction. Invalidates relevant live queries.
Action
External side effects or long-running work.
HTTP
HTTP route using the project's normal authentication boundary.
PublicHTTP
Explicit anonymous HTTP route, often a webhook or public callback.
InternalMutation
Transactional write callable by trusted runtime work such as the scheduler,
but not by browser mutation calls.
LiveGrid
Structured realtime table/grid query for large interactive data surfaces.
Sync
Authorized single-table snapshot plus durable change delivery to IndexedDB.
Args and Results
Use JSON-tagged Go structs:
type CreateTaskArgs struct {
Title string `json:"title"`
}
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
}
func CreateTask(ctx *gonvex.MutationCtx, args CreateTaskArgs) (Task, error) {
// write to tenant-scoped database
}
Generated Bindings
The generated API exposes stable nested function references:
import { api } from "./gonvex/_generated/api";
api.tasks.list
api.tasks.create
api.tasks.grid
api.tasks.recent
Path-safe object references are preferred over stringly-typed calls.
React Usage
import { api } from "./gonvex/_generated/api";
import { useMutation, useQuery } from "./gonvex/_generated/react";
export function TasksPage() {
const tasks = useQuery(api.tasks.list, { status: "open" });
const createTask = useMutation(api.tasks.create);
return (
<TaskList
tasks={tasks ?? []}
onCreate={(title) => createTask({ title })}
/>
);
}
Direct Client Usage
For lower-level surfaces like custom grids:
const unsubscribe = gonvexClient.subscribeQuery(
api.tasks.grid,
{ offset: 0, limit: 300 },
(message) => {
if (message.type === "query.result") {
updateGrid(message.result);
}
},
);
Context
Functions receive runtime context as fields on QueryCtx, MutationCtx,
ActionCtx, and HTTPContext:
import "errors"
func ListTasks(ctx *gonvex.QueryCtx, args ListTasksArgs) ([]Task, error) {
if ctx.User == nil {
return nil, errors.New("authentication required")
}
rows, err := ctx.DB.QueryContext(
ctx.Context,
`SELECT id, title, status FROM tasks WHERE status = $1`,
args.Status,
)
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []Task
for rows.Next() {
var task Task
if err := rows.Scan(&task.ID, &task.Title, &task.Status); err != nil {
return nil, err
}
tasks = append(tasks, task)
}
return tasks, rows.Err()
}
Available context includes:
ProjectID, TenantID, User, Permissions
DB, LandlordDB, TenantDB, Tx
Storage, Data, Sandbox, Scheduler, Ephemeral
Logger, Env, Context
Use ctx.EnvValue("NAME") for project-scoped environment variables with process
environment fallback. Mutations run with ctx.Tx; jobs they enqueue through
ctx.Scheduler are published only after the mutation commits. Long-running
actions can call ctx.NotifyTableChanged(...) after committing intermediate
writes.
Ephemeral state
ctx.Ephemeral stores tenant-local JSON values in Valkey. Project and tenant
namespaces are applied by the runtime, every write requires a positive TTL, and
app code cannot select another scope:
type Lease struct {
UserID string `json:"userId"`
}
func Beat(ctx *gonvex.MutationCtx, _ struct{}) (any, error) {
return nil, ctx.Ephemeral.Set("presence/lease/"+ctx.User.ID, Lease{
UserID: ctx.User.ID,
}, 2*time.Minute)
}
The API is Set(key, value, ttl), Get(key, &target), Delete(key), and
List(prefix). List uses a per-project/tenant sorted set ordered by expiry,
then MGETs only that tenant's live values; it never scans the global Valkey
keyspace. Mark queries that read it with gonvex.ReadsEphemeral() so their
results bypass the durable query cache. Mark mutations that write only
ephemeral state with gonvex.WritesEphemeral() so Gonvex opens no Postgres
transaction, bumps no sync revision, and schedules no reactive invalidation.
Generated Types
Generated function references preserve function kind and path:
api.tasks.list; // { kind: "query", path: "tasks.list" }
api.tasks.create; // { kind: "mutation", path: "tasks.create" }
Use an explicit result type at the hook boundary today:
const tasks = useQuery<Task[]>(api.tasks.list, { status: "open" });
Full argument/result inference from Go structs is not implemented yet. The CLI does generate project API references plus landlord/tenant schema metadata and table-name types.