---
title: "Application Stored Procedures"
description: "Define typed application operations that SQLStyle runs with the current database primary."
section: "Concepts"
source_url: "https://sqlstyle.com/docs/application-stored-procedures"
---
An **Application Stored Procedure (ASP)** is a named, typed database operation
whose handler is application code and whose execution follows the database
primary.

The logic remains ordinary Go or Rust. Nothing is installed into SQLite or
deployed as a separate service.

## Why use an ASP?

A request can arrive at any application instance, while a database write must
run against the current primary. Without an ASP, the application needs its own
primary discovery and RPC path, or a separate service that all writes pass
through.

An ASP makes that boundary part of the database SDK. The application defines
the operation by giving the SDK its name, payload and result types, and handler.
The Go SDK returns a typed function; the Rust SDK returns a typed `Procedure`.
Application code invokes the Go function or calls `Procedure.call` in Rust.

## How a call runs

When the application invokes a generated ASP call, the SDK reads its current
view of primary authority and chooses one of two paths.

### Local primary

The SDK passes the typed payload directly to the handler. No payload or result
serialization is involved.

### Replica

The SDK serializes the payload and sends a named invocation over SQLStyle's
primary stream. At the primary, the receiving SDK resolves the name to the
handler, runs it, and serializes the result back.

## Serialization

The payload and result are application-defined native types. Go and Rust infer
them from the handler and use named-map MessagePack by default; domain types do
not implement SQLStyle-specific serialization methods.

Named-map MessagePack is the recommended general-purpose format in the managed
Go and Rust SDKs. Go uses `msgpack` field tags when present and otherwise reuses
`json` field tags; Rust uses ordinary Serde `Serialize` and `Deserialize`
implementations. Stable field names plus optional or defaulted new fields
support additive compatibility across mixed application versions. Rust also
offers Postcard for fixed schemas. Both SDKs retain JSON when a readable wire
format matters. Go's JSON selection follows `encoding/json` tags and custom
marshaling interfaces. Its options-bearing definition chooses one encoding for
both the payload and result.

Go's `Binary` selection calls the standard `encoding.BinaryAppender` and
`encoding.BinaryUnmarshaler` interfaces directly. `CustomEncoding` retains the
typed serializer contract for generated and handwritten formats. Remote append
encoders receive a reusable SDK-owned destination.

### Select another encoding

#### Go

```go
addTodo, err := sqlstyle.DefineIASPWithOptions(
    config,
    "todos.add-json",
    sqlstyle.ProcedureOptions{Encoding: sqlstyle.JSON},
    handler,
)
```

#### Rust

```rust
let options = sqlstyle::ProcedureOptions::new(
    sqlstyle::JsonSerializer,
    sqlstyle::JsonSerializer,
    sqlstyle::JsonSerializer,
);
let add_todo = sqlstyle::define_iasp_with_options(
    sqlstyle::drivers::Rusqlite,
    config,
    "todos.add-json",
    options,
    handler,
)?;
```

## When no primary is available

If no writable primary can accept the invocation, the SDK waits for authority
to change instead of failing immediately. One wait budget covers primary
discovery, write admission, and retries after a transient busy or unavailable
response. The default budget is eight seconds.

If a primary becomes ready within the budget, the invocation continues.
Otherwise the SDK returns its primary-unavailable error. A zero budget fails
immediately; in Go, context cancellation or an earlier deadline ends the wait
sooner.

Set the budget in the [SDK open
configuration](#primary-wait-timeout-configuration). It is not a shared JSON5
configuration field.

### Primary wait timeout configuration

#### Go

```go
wait := 3 * time.Second
// wait := time.Duration(0) // Fail immediately.
config.Options.PrimaryWaitTimeout = &wait
```

#### Rust

```rust
use std::time::Duration;

config.options.primary_wait_timeout = Duration::from_secs(3);
// config.options.primary_wait_timeout = Duration::ZERO; // Fail immediately.
```

## Example

### Go

```go
type AddTodo struct {
    Title string `json:"title"`
}

type AddTodoResult struct {
    ID int64 `json:"id"`
}

addTodo, err := sqlstyle.DefineIASP(
	config,
	"todos.add",
    func(ctx context.Context, tx *sql.Tx, payload AddTodo) (AddTodoResult, error) {
        result, err := tx.ExecContext(ctx,
            "INSERT INTO todos(title) VALUES (?)", payload.Title)
        if err != nil { return AddTodoResult{}, err }
        id, err := result.LastInsertId()
        return AddTodoResult{ID: id}, err
    },
)
if err != nil { return err }

result, err := addTodo(ctx, AddTodo{Title: "ship it"})
id := result.ID
```

### Rust

```rust
#[derive(serde::Serialize, serde::Deserialize)]
struct AddTodo { title: String }

#[derive(serde::Serialize, serde::Deserialize)]
struct AddTodoResult { id: i64 }

let add_todo = sqlstyle::define_iasp(
    sqlstyle::drivers::Rusqlite,
    config.clone(),
    "todos.add",
    |transaction, payload: &AddTodo| -> sqlstyle::asp::Result<AddTodoResult> {
        transaction.execute(
            "INSERT INTO todos(title) VALUES (?1)",
            [&payload.title],
        )?;
        Ok(AddTodoResult { id: transaction.last_insert_rowid() })
    },
)?;

let result = add_todo.call(&AddTodo {
    title: "ship it".into(),
})?;
let id = result.id;
```

Defining a procedure stores its configuration but opens no database
connection. All procedures for that database share an on-demand pool, which
defaults to eight connections, no minimum idle connections, and a 30-second
idle timeout.

When the final ordinary application connection closes, SQLStyle stops new
procedure admission and waits up to 20 seconds for admitted handlers. It then
interrupts remaining database work and waits for every handler to return before
finishing shutdown. A later call recreates the procedure pool from `Config`.

## ASP or IASP

Choose based on who owns the transaction and duplicate handling.

- **IASP:** SQLStyle owns the transaction and makes remote execution
  idempotent. The handler must not commit or roll back.
- **ASP:** Your application owns the transaction and duplicate handling.
  Use it when the operation already has an idempotency key or duplicate policy.