Overview
There are three types of server functions:| Type | Context | Use case |
|---|---|---|
| Query | Read-only database | Fetch data — leaderboards, player lists, settings |
| Mutation | Read/write database | Create, update, or delete rows — add a player, update a score |
| Action | Read/write database, fetch, secrets | Side effects — call external APIs, send webhooks, process payments |
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 runvision init, select “Include server functions” when prompted. This creates a server/ directory with starter files:
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 inserver/schema.ts. Each table has typed fields and optional indexes.
Supported Field Types
| Validator | Type | Description |
|---|---|---|
v.string() | string | Text values |
v.number() | number | Finite numbers |
v.boolean() | boolean | True or false |
v.literal(value) | Exact value | A specific string, number, or boolean |
v.array(validator) | T[] | Array of a given type |
v.object(shape) | { ... } | Nested object with typed fields |
v.union(...validators) | T1 | T2 | ... | One of several types |
v.nullable(validator) | T | null | Nullable variant of any type |
v.any() | unknown | Any JSON value |
.optional():
Indexes
Add indexes to tables for efficient querying. An index references one or more columns:Storage Scopes
By default, table data is scoped to the account (channel). You can change how data is partitioned by setting the storage scope:| Scope | Partition Key | Description |
|---|---|---|
"instance" | layerId | Data is isolated per extension layer instance |
"overlay" | overlayId | Data is shared across extension layers in the same overlay |
"account" | channelId | Data is shared across all overlays for the same channel (default) |
"global" | extensionId | Data is shared across ALL installs of this extension |
.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
useQuery:
Array of field/operator/value conditions. See Filter syntax
below.
Maximum rows to return.
Skip rows (for pagination).
Restrict to channel editors/owner.
insertRow — Create a row
useMutation:
patchRow — Update a row
useMutation:
deleteRow — Delete a row
useMutation:
fetchAction — Outbound HTTP request
Make external API calls from the server. Requires configuring an egress allowlist.useMutation:
The URL to fetch. Can be a literal string,
arg("paramName"), or built from a
secret.HTTP method. Defaults to
"GET".Request headers.
Request body.
How to parse the response. Defaults to
"json".Value Expressions
Dynamic values that are resolved at runtime:| Helper | Description | Example |
|---|---|---|
arg("name") | Reference a function argument passed by the caller | arg("userId") |
identity("key") | Reference a field from the caller’s identity | identity("subject") |
secret("KEY") | Reference a stored secret (set in the dashboard) | secret("API_KEY") |
"literal" | A literal value baked into the function | true, 0, "active" |
"subject", "channelId", "extensionId", "installationId", "overlayId", "layerId".
Filter Syntax
Usefilter() to build query conditions for queryRows:
"eq", "neq", "lt", "lte", "gt", "gte", "in"
Declarative vs Handler-based
| Declarative | Handler-based | |
|---|---|---|
| Syntax | queryRows(...), insertRow(...), etc. | query({ handler }), mutation({ handler }) |
| Real-time subscriptions | Yes | Not yet |
| Custom logic | No — values and filters only | Full JavaScript in the handler |
| Use when | Standard CRUD and simple queries | Complex logic, conditional writes, multi-step operations |
server/functions.ts file.
Egress Allowlist
fetchAction can only reach hosts on your extension’s allowlist. Configure it in vision.config.json:
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 — usequery(), mutation(), and action() with handler functions.
Queries
Queries read data from the database. They receive aQueryContext 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
| Method | Signature | Description |
|---|---|---|
db.get | (table: string, id: string) => Promise<object | null> | Fetch a single row by ID |
db.query | (table: string, request?) => Promise<object[]> | Query rows with optional filters, indexes, limit, and offset |
Query Request Options
| Field | Type | Description |
|---|---|---|
index | { name, values } | Use a named index with optional key values |
filters | QueryFilter[] | Array of field/operator/value conditions |
limit | number | Max rows to return |
offset | number | Skip rows (for pagination) |
Filter Operators
eq, neq, lt, lte, gt, gte, in
Mutations
Mutations read and write data. They receive aMutationContext 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:| Method | Signature | Description |
|---|---|---|
db.insert | (table: string, value: object) => Promise<{ id: string }> | Insert a row and return its generated ID |
db.patch | (table: string, id: string, value: object) => Promise<void> | Update specific fields on a row |
db.delete | (table: string, id: string) => Promise<void> | Delete a row by ID |
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 declarativefetchAction 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:| Property | Type | Description |
|---|---|---|
fetch | (input: RequestInfo, init?: RequestInit) => Promise<Response> | Restricted HTTP client (HTTPS only, no private IPs) |
secrets | { get(name: string): Promise<string | undefined> } | Read extension secrets set in the dashboard |
Auth
Every server function context includes anauth API for identity and authorization:
Auth API
| Method | Signature | Description |
|---|---|---|
auth.getUserIdentity | () => Promise<UserIdentity | null> | Returns the current user’s identity, or null if unauthenticated |
auth.hasScope | (scope: string) => boolean | Check if the caller has a specific scope |
UserIdentity
| Property | Type | Description |
|---|---|---|
subject | string | Unique user identifier |
issuer | string? | Token issuer |
audience | string? | Token audience |
Scoping Functions
Restrict a function to callers with specific scopes: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.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.editorOnly with scope for additional restrictions:
Argument Validation
All function arguments are validated at runtime using the samev 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 toaction 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
^[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:- The client request is authenticated and scoped to the extension and channel
- The function executes in an isolated runtime with access only to its own database
- Results are returned to the client