Gonvex

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.

On this page