> ## Documentation Index
> Fetch the complete documentation index at: https://docs.1up.vision/llms.txt
> Use this file to discover all available pages before exploring further.

# Server Functions

> Add a backend to your extension with server-side queries, mutations, and actions — backed by a per-install edge database.

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:

| 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 |

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:

```
my-extension/
  src/
    admin.tsx
    layer.tsx
  server/
    schema.ts          # Database schema
    functions.ts       # Server functions
  vision.config.json
```

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`:

```ts theme={null}
import { defineSchema, defineTable, v } from "@1upvision/sdk/server";
import {
  queryRows,
  insertRow,
  patchRow,
  deleteRow,
  fetchAction,
  arg,
  identity,
  secret,
  filter,
} from "@1upvision/sdk/server";
import { query, mutation, action } from "@1upvision/sdk/server";
```

***

## Schema

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

```ts theme={null}
import { defineSchema, defineTable, v } from "@1upvision/sdk/server";

export default defineSchema({
  players: defineTable({
    name: v.string(),
    score: v.number(),
    active: v.boolean(),
  }).index("by_score", ["score"]),

  matches: defineTable({
    playerIds: v.array(v.string()),
    winnerId: v.string().optional(),
    startedAt: v.number(),
  }),
});
```

### 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                        |

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

```ts theme={null}
defineTable({
  name: v.string(),
  bio: v.string().optional(), // string | undefined
  avatar: v.nullable(v.string()), // string | null
});
```

### Indexes

Add indexes to tables for efficient querying. An index references one or more columns:

```ts theme={null}
defineTable({
  userId: v.string(),
  score: v.number(),
})
  .index("by_user", ["userId"])
  .index("by_score", ["score"]);
```

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:

| 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              |

Set the storage scope via the options object or the chainable `.storage()` method:

```ts theme={null}
// Via options object
defineTable(
  {
    key: v.string(),
    value: v.string(),
  },
  { storage: "global" },
);

// Via chainable method
defineTable({
  key: v.string(),
  value: v.string(),
}).storage("overlay");

// Default is "account" — no need to specify
defineTable({
  userId: v.string(),
  score: v.number(),
});
```

<Info>
  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.
</Info>

***

## 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.

```ts theme={null}
import {
  queryRows,
  insertRow,
  patchRow,
  deleteRow,
  arg,
  filter,
} from "@1upvision/sdk/server";
```

### queryRows — Read from a table

```ts theme={null}
// Get all players
export const getPlayers = queryRows("players");

// Get players with a filter and limit
export const getActivePlayers = queryRows("players", {
  filters: [filter("active", "eq", true)],
  limit: 50,
});

// Get players filtered by a function argument
export const getPlayersByTeam = queryRows("players", {
  filters: [filter("team", "eq", arg("team"))],
});
```

Call with `useQuery`:

```tsx theme={null}
const { data: players } = useQuery("getPlayers");
const { data: team } = useQuery("getPlayersByTeam", { team: "red" });
```

**Options:**

<ParamField path="filters" type="FilterDefinition[]" optional>
  Array of field/operator/value conditions. See [Filter syntax](#filter-syntax)
  below.
</ParamField>

<ParamField path="limit" type="number" optional>
  Maximum rows to return.
</ParamField>

<ParamField path="offset" type="number" optional>
  Skip rows (for pagination).
</ParamField>

<ParamField path="editorOnly" type="boolean" optional>
  Restrict to channel editors/owner.
</ParamField>

### insertRow — Create a row

```ts theme={null}
export const addPlayer = insertRow("players", {
  name: arg("name"),
  score: arg("score"),
  active: true,
});
```

Call with `useMutation`:

```tsx theme={null}
const { mutate: addPlayer } = useMutation("addPlayer");
addPlayer({ name: "Alice", score: 100 });
```

### patchRow — Update a row

```ts theme={null}
export const updateScore = patchRow("players", arg("id"), {
  score: arg("score"),
});
```

Call with `useMutation`:

```tsx theme={null}
const { mutate: updateScore } = useMutation("updateScore");
updateScore({ id: "abc123", score: 200 });
```

### deleteRow — Delete a row

```ts theme={null}
export const removePlayer = deleteRow("players", arg("id"));
```

Call with `useMutation`:

```tsx theme={null}
const { mutate: removePlayer } = useMutation("removePlayer");
removePlayer({ id: "abc123" });
```

### fetchAction — Outbound HTTP request

Make external API calls from the server. Requires configuring an [egress allowlist](#egress-allowlist).

```ts theme={null}
import { fetchAction, secret } from "@1upvision/sdk/server";

export const getWeather = fetchAction(
  {
    url: "https://api.weather.com/current",
    headers: { "X-Api-Key": secret("WEATHER_API_KEY") },
  },
  { response: "json" },
);
```

Call with `useMutation`:

```tsx theme={null}
const { mutateAsync: getWeather } = useMutation("getWeather");
const data = await getWeather();
```

**Request config:**

<ParamField path="url" type="string | arg() | secret()" required>
  The URL to fetch. Can be a literal string, `arg("paramName")`, or built from a
  secret.
</ParamField>

<ParamField path="method" type="string" optional>
  HTTP method. Defaults to `"GET"`.
</ParamField>

<ParamField path="headers" type="Record<string, string | arg() | secret()>" optional>
  Request headers.
</ParamField>

<ParamField path="body" type="string | arg()" optional>
  Request body.
</ParamField>

**Options:**

<ParamField path="response" type="'json' | 'text' | 'none'" optional>
  How to parse the response. Defaults to `"json"`.
</ParamField>

### 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"` |

Common identity keys: `"subject"`, `"channelId"`, `"extensionId"`, `"installationId"`, `"overlayId"`, `"layerId"`.

### Filter Syntax

Use `filter()` to build query conditions for `queryRows`:

```ts theme={null}
import { filter, arg, identity } from "@1upvision/sdk/server";

filter("userId", "eq", arg("userId")); // Field equals an argument
filter("owner", "eq", identity("subject")); // Field equals the caller
filter("score", "gt", 100); // Field greater than a literal
filter("status", "in", ["active", "pending"]); // Field is one of several values
```

**Operators:** `"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 |

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`:

```json theme={null}
{
  "server": true,
  "egress": {
    "allowHosts": ["api.weather.com", "*.example.com"],
    "allowPorts": [443]
  }
}
```

After changing the allowlist, rebuild with `vision dev` or `vision deploy`.

### Complete Declarative Example

```ts theme={null}
import {
  queryRows,
  insertRow,
  patchRow,
  deleteRow,
  arg,
  filter,
  identity,
} from "@1upvision/sdk/server";

export const getRules = queryRows("rules", {
  filters: [filter("channelId", "eq", identity("channelId"))],
});

export const addRule = insertRow(
  "rules",
  {
    text: arg("text"),
    revealed: false,
    channelId: identity("channelId"),
  },
  { editorOnly: true },
);

export const revealRule = patchRow(
  "rules",
  arg("id"),
  { revealed: true },
  { editorOnly: true },
);

export const removeRule = deleteRow("rules", arg("id"), {
  editorOnly: true,
});
```

***

## 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.

<Info>
  For simple reads, prefer [declarative
  `queryRows`](#queryrows--read-from-a-table) — it's less code and supports
  real-time subscriptions. Use handler-based queries when you need custom logic.
</Info>

```ts theme={null}
import { query, v } from "@1upvision/sdk/server";

export const getPlayers = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("players");
  },
});

export const getPlayerById = query({
  args: {
    id: v.string(),
  },
  handler: async (ctx, args) => {
    return await ctx.db.get("players", args.id);
  },
});
```

### Querying with Filters

```ts theme={null}
export const getActivePlayers = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("players", {
      filters: [{ field: "active", op: "eq", value: true }],
      limit: 50,
    });
  },
});
```

### Querying with Indexes

```ts theme={null}
export const getTopScores = query({
  args: {
    limit: v.number(),
  },
  handler: async (ctx, args) => {
    return await ctx.db.query("players", {
      index: { name: "by_score", values: [] },
      limit: args.limit,
    });
  },
});
```

### 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 a `MutationContext` with the full `db` API. For simple inserts, updates, and deletes, prefer the [declarative builders](#insertrow--create-a-row) instead.

```ts theme={null}
import { mutation, v } from "@1upvision/sdk/server";

export const addPlayer = mutation({
  args: {
    name: v.string(),
    score: v.number(),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("players", {
      name: args.name,
      score: args.score,
      active: true,
    });
  },
});

export const updateScore = mutation({
  args: {
    id: v.string(),
    score: v.number(),
  },
  handler: async (ctx, args) => {
    await ctx.db.patch("players", args.id, { score: args.score });
  },
});

export const removePlayer = mutation({
  args: {
    id: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.delete("players", args.id);
  },
});
```

### 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 declarative [`fetchAction`](#fetchaction--outbound-http-request) instead. Use handler-based actions for complex flows — conditional logic, multiple API calls, or processing responses before storing data.

```ts theme={null}
import { action, v } from "@1upvision/sdk/server";

export const notifyDiscord = action({
  args: {
    message: v.string(),
  },
  handler: async (ctx, args) => {
    const webhookUrl = await ctx.secrets.get("DISCORD_WEBHOOK_URL");
    if (!webhookUrl) throw new Error("Discord webhook not configured");

    await ctx.fetch(webhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ content: args.message }),
    });

    return { sent: true };
  },
});
```

### 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         |

<Warning>
  `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](#egress-allowlist).
</Warning>

***

## Auth

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

```ts theme={null}
export const getMyData = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();

    if (!identity) {
      throw new Error("Not authenticated");
    }

    return await ctx.db.query("data", {
      filters: [{ field: "userId", op: "eq", value: identity.subject }],
    });
  },
});
```

### 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:

```ts theme={null}
export const adminReset = mutation({
  scope: "admin",
  args: {},
  handler: async (ctx) => {
    // Only callable by users with the "admin" scope
    const players = await ctx.db.query("players");
    for (const player of players) {
      await ctx.db.patch("players", player.id as string, { score: 0 });
    }
  },
});
```

You can require multiple scopes by passing an array:

```ts theme={null}
export const dangerousAction = action({
  scope: ["admin", "superuser"],
  // ...
});
```

### 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.

```ts theme={null}
export const resetScores = mutation({
  editorOnly: true,
  args: {},
  handler: async (ctx) => {
    const players = await ctx.db.query("players");
    for (const player of players) {
      await ctx.db.patch("players", player.id as string, { score: 0 });
    }
  },
});
```

This works with both handler-based and declarative functions:

```ts theme={null}
export const clearLeaderboard = deleteRow("players", arg("playerId"), {
  editorOnly: true,
});
```

<Info>
  `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.
</Info>

You can combine `editorOnly` with `scope` for additional restrictions:

```ts theme={null}
export const dangerousReset = mutation({
  editorOnly: true,
  scope: "admin",
  handler: async (ctx) => {
    // Only editors with the "admin" scope can call this
  },
});
```

***

## 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.

```ts theme={null}
export const createMatch = mutation({
  args: {
    playerIds: v.array(v.string()),
    bestOf: v.number(),
    ranked: v.boolean().optional(),
  },
  handler: async (ctx, args) => {
    // args.playerIds is string[], args.bestOf is number
    // args.ranked is boolean | undefined
    return await ctx.db.insert("matches", {
      playerIds: args.playerIds,
      bestOf: args.bestOf,
      ranked: args.ranked ?? false,
      startedAt: Date.now(),
    });
  },
});
```

***

## CLI Commands

### `vision dev`

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

```bash theme={null}
vision dev
```

### `vision run`

Execute a server function from the terminal during development:

```bash theme={null}
# Run a query
vision run query:getPlayers

# Run a mutation with arguments
vision run mutation:addPlayer '{"name": "Alice", "score": 100}'

# Run an action
vision run action:notifyDiscord '{"message": "Hello from CLI!"}'
```

### `vision deploy`

Build and deploy a production version:

```bash theme={null}
vision deploy
```

***

## 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()`.

<Info>
  Secrets are **server-only**. They can only be accessed in `action` functions
  via `ctx.secrets.get()` and are never exposed to client-side code.
</Info>

### 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.
