Skip to main content
Server functions give your extension a backend. Define a schema, write typed functions, and the Vision runtime handles the rest — each installation gets its own isolated database at the edge.

Overview

There are three types of server functions: All functions receive a typed context object and validated arguments. Declarative queries (built with queryRows) support real-time subscriptions — when data changes, connected clients update automatically. Handler-based queries (built with query()) work for on-demand fetching but do not support real-time subscriptions yet.

Getting Started

Scaffold with the CLI

When you run vision init, select “Include server functions” when prompted. This creates a server/ directory with starter files:
You can also add server functions to an existing project by creating the server/ directory manually and setting "server": true in your vision.config.json.

Imports

Server functions and schema utilities are imported from @1upvision/sdk/server:

Schema

Define your database tables in server/schema.ts. Each table has typed fields and optional indexes.

Supported Field Types

Any validator can be made optional by chaining .optional():

Indexes

Add indexes to tables for efficient querying. An index references one or more columns:
Use indexes when querying to avoid full table scans.

Storage Scopes

By default, table data is scoped to the account (channel). You can change how data is partitioned by setting the storage scope: Set the storage scope via the options object or the chainable .storage() method:
When you change a table’s storage scope, existing data will not be automatically migrated. The runtime uses a new partition key, so data written under the previous scope will no longer be visible.

Declarative Functions

The simplest way to define server functions. Instead of writing handler logic, you describe what you want and the runtime executes it. Declarative queries also support real-time subscriptions — connected clients update automatically when data changes.

queryRows — Read from a table

Call with useQuery:
Options:
FilterDefinition[]
Array of field/operator/value conditions. See Filter syntax below.
number
Maximum rows to return.
number
Skip rows (for pagination).
boolean
Restrict to channel editors/owner.

insertRow — Create a row

Call with useMutation:

patchRow — Update a row

Call with useMutation:

deleteRow — Delete a row

Call with useMutation:

fetchAction — Outbound HTTP request

Make external API calls from the server. Requires configuring an egress allowlist.
Call with useMutation:
Request config:
string | arg() | secret()
required
The URL to fetch. Can be a literal string, arg("paramName"), or built from a secret.
string
HTTP method. Defaults to "GET".
Record<string, string | arg() | secret()>
Request headers.
string | arg()
Request body.
Options:
'json' | 'text' | 'none'
How to parse the response. Defaults to "json".

Value Expressions

Dynamic values that are resolved at runtime: Common identity keys: "subject", "channelId", "extensionId", "installationId", "overlayId", "layerId".

Filter Syntax

Use filter() to build query conditions for queryRows:
Operators: "eq", "neq", "lt", "lte", "gt", "gte", "in"

Declarative vs Handler-based

You can mix both styles in the same server/functions.ts file.

Egress Allowlist

fetchAction can only reach hosts on your extension’s allowlist. Configure it in vision.config.json:
After changing the allowlist, rebuild with vision dev or vision deploy.

Complete Declarative Example


Handler-based Functions

For complex logic that can’t be expressed declaratively — conditional writes, multi-step operations, or custom validation — use query(), mutation(), and action() with handler functions.

Queries

Queries read data from the database. They receive a QueryContext with a read-only db and auth API.
For simple reads, prefer declarative queryRows — it’s less code and supports real-time subscriptions. Use handler-based queries when you need custom logic.

Querying with Filters

Querying with Indexes

Database Reader API

Query Request Options

Filter Operators

eq, neq, lt, lte, gt, gte, in

Mutations

Mutations read and write data. They receive a MutationContext with the full db API. For simple inserts, updates, and deletes, prefer the declarative builders instead.

Database Writer API

Extends the reader API with write operations:

Actions

Actions have full database access plus the ability to make HTTP requests and read secrets. For simple HTTP requests without custom logic, you can use the declarative fetchAction instead. Use handler-based actions for complex flows — conditional logic, multiple API calls, or processing responses before storing data.

Action Context

Actions receive everything mutations get, plus:
ctx.fetch is sandboxed. It enforces HTTPS, blocks requests to private IP ranges and metadata endpoints, and only allows hosts on your extension’s egress allowlist.

Auth

Every server function context includes an auth API for identity and authorization:

Auth API

UserIdentity

Scoping Functions

Restrict a function to callers with specific scopes:
You can require multiple scopes by passing an array:

Editor-Only Functions

Restrict a function so only verified editors of the channel (users with an assigned role) and the channel owner can invoke it. All other callers receive a 403 Forbidden response.
This works with both handler-based and declarative functions:
editorOnly is enforced at both the API layer and the edge runtime. Even if the caller has valid authentication, they must be a verified editor of the channel to invoke editor-only functions.
You can combine editorOnly with scope for additional restrictions:

Argument Validation

All function arguments are validated at runtime using the same v validators used in schemas. Invalid arguments are rejected before the handler runs.

CLI Commands

vision dev

Watches your project, builds client and server bundles, pushes the schema, and uploads to the dev environment on every change.

vision run

Execute a server function from the terminal during development:

vision deploy

Build and deploy a production version:

Environment Variables (Secrets)

Secrets are managed in the extension dashboard under Settings > Environment. They are encrypted at rest and only available to action functions via ctx.secrets.get().
Secrets are server-only. They can only be accessed in action functions via ctx.secrets.get() and are never exposed to client-side code.

Managing Secrets

Use the Environment page in the extension settings sidebar to:
  • Add new secrets with uppercase keys (e.g., DISCORD_WEBHOOK_URL, API_TOKEN)
  • Update existing secret values
  • Delete secrets that are no longer needed
Keys must match the pattern ^[A-Z0-9_]+$ — uppercase letters, numbers, and underscores only.

How It Works

Each extension installation gets its own isolated database at the edge. Data partitioning is handled by the storage scope configured on each table. When a server function is invoked:
  1. The client request is authenticated and scoped to the extension and channel
  2. The function executes in an isolated runtime with access only to its own database
  3. Results are returned to the client
Mutations trigger real-time updates — any active query subscriptions are automatically re-evaluated and pushed to connected clients.