zyn

An application framework
for AI agents.

Build websites and APIs in Zig. Your application compiles to a native executable.

Requires Zig 0.16.0

src/routes.zigZig

const zyn = @import("zyn");

pub const table: zyn.RouteTable = &.{
    .get("/hello/:name", hello, &.{}),
};

fn hello(action: *zyn.Action) !zyn.Response {
    return action.json(.{
        .message = "Hello",
        .name = action.param("name"),
    }, .{});
}

GET /hello/Ari200 OK

{ "message": "Hello", "name": "Ari" }

Build an auction house.

Show the catalogue

src/lots.zig
pub const Lot = struct {
    id: []const u8,
    title: []const u8,
    description: []const u8,
    highest_bid: i64,
    bidder_id: ?[]const u8 = null,
    closed: bool,
};
src/routes.zig
pub fn index(action: *zyn.Action) !zyn.Response {
    const catalogue = try action.db.many(lots.Lot,
        \\SELECT id, title, description, highest_bid, bidder_id, closed
        \\FROM lots ORDER BY id LIMIT 20
    , .{});
    return action.render("catalogue.html", .{ .lots = catalogue }, .{});
}
The HTML view

src/views/catalogue.html.ez

<article class="catalogue">
    <header class="catalogue-heading">
        <p class="cabinet-eyebrow">Est. sometime before tomorrow · Auction No. 013</p>
        <h3>Cabinet of Curiosities</h3>
        <p>Objects of uncertain provenance.</p>
        <span class="cabinet-seal" aria-hidden="true">✦</span>
    </header>
    <ul class="lot-list">
        <% foreach (@lots) |lot| { %>
            <li>
                <div class="curiosity-art" data-curiosity="<%= lot["id"] %>" aria-hidden="true">
                    <svg viewBox="0 0 160 120" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                        <% if (lot["id"] == "haunted-toaster") { %>
                            <ellipse cx="80" cy="105" rx="46" ry="5" fill="currentColor" opacity=".1" stroke="none"/>
                            <path d="M58 49V32c-13-10 0-22 13-18 8-8 29-4 29 6 12 1 17 13 4 17v12" fill="#f0c58b"/>
                            <path d="M46 53c0-10 68-10 68 0v40H46Z" fill="#c2cdc0"/><path d="M57 53h46M57 93v7m45-7v7m12-34h9v15h-9"/>
                            <path d="M69 72v4m20-4v4m-13 7q4 4 8 0M33 29l-5 7m97-15 5 6M128 44h8m-4-4v8"/>
                        <% } else if (lot["id"] == "warm-meteorite") { %>
                            <ellipse cx="80" cy="105" rx="39" ry="5" fill="currentColor" opacity=".1" stroke="none"/>
                            <path d="m45 67 17-28 33-8 24 26-8 31-35 9-29-14Z" fill="#dca18b"/>
                            <path d="m62 39 8 24-23 20m23-20 25-32m-25 32 21 14 20 11m-20-11 28-20M76 97l15-20" opacity=".55"/>
                            <path d="M66 25c-9-9 8-9 0-18m22 14c-9-9 8-9 0-18m37 28h10m-5-5v10M31 51h6"/>
                        <% } else { %>
                            <ellipse cx="80" cy="105" rx="44" ry="5" fill="currentColor" opacity=".1" stroke="none"/>
                            <g transform="rotate(-9 80 60)"><path d="M35 35h90v58H35Z" fill="#efe1b8"/><path d="m35 35 45 35 45-35M35 93l29-32m61 32L96 61"/><circle cx="80" cy="68" r="9" fill="#bb6e61"/><path d="m77 68 3-3 3 3-3 4Z" stroke="#fff1d7" stroke-width="1.5"/><path d="M107 43h10v12h-10Z" stroke-dasharray="2 3"/></g>
                            <path d="M41 16h10m-5-5v10m82 1 3 4m-106 45 5 3"/>
                        <% } %>
                    </svg>
                </div>
                <div class="lot-copy">
                    <h4><%= lot["title"] %></h4>
                    <p><%= lot["description"] %></p>
                    <p class="lot-meta"><span><% if (lot["closed"]) { %>Closed<% } else { %>Bidding open<% } %></span><strong>£<%= lot["highest_bid"] %></strong></p>
                </div>
            </li>
        <% } %>
    </ul>
</article>
The database tables

db/schema.sql

CREATE TABLE bidders (
    id text PRIMARY KEY,
    email text NOT NULL
);

CREATE TABLE lots (
    id text PRIMARY KEY,
    title text NOT NULL,
    description text NOT NULL,
    highest_bid bigint NOT NULL CHECK (highest_bid >= 0),
    bidder_id text REFERENCES bidders(id),
    closes_at timestamptz NOT NULL,
    closed boolean NOT NULL DEFAULT false
);
The catalogue

Est. sometime before tomorrow · Auction No. 013

Cabinet of Curiosities

Objects of uncertain provenance.

  • Haunted toaster

    Two slices. Three shadows.

    Bidding open£25

  • Suspiciously warm meteorite

    Recovered in 1894. Still cooling.

    Bidding open£120

  • A letter addressed to you

    Unopened. Postmarked tomorrow.

    Bidding open£10

Make your bid

src/routes.zig · bid
pub fn bid(action: *zyn.Action) !zyn.Response {
    const payload = try action.expect_payload(struct { amount: u32 });
    var tx = try action.db.begin();
    defer tx.cancel();
    if (!try lots.place_bid(&tx, action.param("id"), action.session.bidder_id.?, payload.amount))
        return action.text("Bid higher. Unless the auction is over.", .{ .status = .conflict });
    try tx.commit();
    try action.flash.add(.success, "Your bid stands. For now.");
    return action.redirect(.see_other, try zyn.url(
        action.arena,
        "/lots/:id",
        .{ .id = action.param("id") },
    ));
}
One bid at a time

src/lots.zig

pub fn place_bid(tx: *zyn.Database.Transaction, id: []const u8, bidder: []const u8, amount: u32) !bool {
    // Acquire the lock before checking the deadline, including time spent waiting.
    _ = (try tx.one(struct { id: []const u8 },
        \\SELECT id FROM lots WHERE id = $1 FOR UPDATE
    , .{id})) orelse return false;
    return 1 == try tx.exec_count(
        \\UPDATE lots SET highest_bid = $1, bidder_id = $2
        \\WHERE id = $3 AND NOT closed
        \\AND closes_at > clock_timestamp() AND highest_bid < $1
    , .{ @as(i64, amount), bidder, id });
}
After bidding £40 · preview

✦ Cabinet of Curiosities ✦

Haunted toaster

Two slices. Three shadows.

Your bid stands. For now.

Current bid£40

Close the auction

src/jobs.zig · close_lot
pub fn close_lot(job: *zyn.Job, args: Args) !void {
    var tx = try job.db.begin();
    defer tx.cancel();
    const lot = (try tx.one(struct { closed: bool },
        \\SELECT closed FROM lots WHERE id = $1 FOR UPDATE
    , .{args.lot_id})) orelse return;
    if (lot.closed) return;
    const winner = (try tx.one(struct { bidder_id: ?[]const u8 },
        \\UPDATE lots SET closed = true
        \\WHERE id = $1 AND closes_at <= clock_timestamp()
        \\RETURNING bidder_id
    , .{args.lot_id})) orelse return error.AuctionStillOpen;
    if (winner.bidder_id != null)
        try tx.notifications.deliver_later("lot_won", .{ .lot_id = args.lot_id });
    try tx.commit();
}
Schedule the close

src/lots.zig · seed

try tx.jobs.enqueue("close_lot", .{ .lot_id = lot.id }, .{ .at = closes_at });
The notification

src/notifications.zig

pub fn lot_won(notification: *zyn.Notification, args: Args) !void {
    const lot = (try notification.db.one(struct { title: []const u8, highest_bid: i64, email: []const u8 },
        \\SELECT lots.title, lots.highest_bid, bidders.email
        \\FROM lots JOIN bidders ON bidders.id = lots.bidder_id
        \\WHERE lots.id = $1 AND lots.closed
    , .{args.lot_id})) orelse return;
    try notification.email(.{ .title = lot.title, .amount = lot.highest_bid }, .{
        .to = &.{zyn.Email.Address.init(lot.email)},
        .subject = "You have acquired a curiosity",
    });
}
The winner’s email

And let an agent work on the code.

Find the application code.

$ zyn context routesOutput excerpt
# Routes context

## Implementation map

### `src/routes.zig`

- `GET /` → `index` at `src/routes.zig:14:8`
  - Registered at: `src/routes.zig:7:5`
  - Pipeline (declaration order): no route guards or hooks declared
- `GET /lots/:id` → `show` at `src/routes.zig:23:8`
  - Registered at: `src/routes.zig:8:5`
  - Pipeline (declaration order): no route guards or hooks declared
- `POST /lots/:id/bids` → `bid` at `src/routes.zig:36:8`
  - Registered at: `src/routes.zig:10:5`
  - Pipeline (declaration order): guard `require_paddle` at `src/routes.zig:58:8`
- `POST /paddle` → `take_paddle` at `src/routes.zig:53:8`
  - Registered at: `src/routes.zig:9:5`
  - Pipeline (declaration order): no route guards or hooks declared
Project context

Look up the framework.

$ zyn docs search "Action.render"
# Documentation search

## Guides (1)

- `framework/views` — Views
  Action.render and `Hook.render` require a comptime-known template name. The
  → `zyn docs framework/views`

## API (1)

- `zyn.Action.render`
  Renders a view and returns its completed response.
  → `zyn docs api zyn.Action.render`
Documentation at the command line

Search docs