Buildable example

Blog — ZigBase examples

A minimal app on ZigBase-as-a-library: a slugify hook and a Zigapagos frontend served from a runtime-selected directory.

A minimal application built on ZigBase as a library. It imports the zigbase module, then registers a single before_create record hook on the posts collection that derives a URL slug from the post title when one isn’t supplied. Everything else — HTTP API, SQLite storage, auth, CLI — comes straight from the framework.

The blog example's retro 90s GeoCities-style home page: a 'Latest posts' list of post cards with titles, dates, and excerpts, served by the blog binary itself
The blog example’s retro 90s GeoCities-style home page: a ‘Latest posts’ list of post cards with titles, dates, and excerpts, served by the blog binary itself

What it proves

This example exists primarily as a packaging proof: building it demonstrates that the SQLite C sources and the zap dependency travel transitively through the published zigbase module into a downstream consumer package. If this compiles and runs, ZigBase works as a real dependency.

Pre-1.0: ZigBase is pre-1.0 — the hook-config shape and module API may change between releases.

The hook

const std = @import("std");
const zigbase = @import("zigbase");

fn slugify(ctx: *zigbase.Ctx, ev: *zigbase.RecordEvent) anyerror!void {
    _ = ctx;
    if (ev.record.* != .object) return;
    if (ev.record.object.get("slug") != null) return;
    const title = if (ev.record.object.get("title")) |t| switch (t) {
        .string => |s| s,
        else => return,
    } else return;

    // Record mutations MUST allocate with `ev.arena.a` (`ev.arena` is a typed
    // request arena; `.a` is the allocator that owns `ev.record`).
    const buf = try ev.arena.a.alloc(u8, title.len);
    var len: usize = 0;
    var in_run = false;
    for (title) |ch| {
        if (std.ascii.isAlphanumeric(ch)) {
            buf[len] = std.ascii.toLower(ch);
            len += 1;
            in_run = true;
        } else if (in_run) {
            buf[len] = '-';
            len += 1;
            in_run = false;
        }
    }
    if (len > 0 and buf[len - 1] == '-') len -= 1;
    try ev.record.object.put(ev.arena.a, "slug", .{ .string = buf[0..len] }); // "Hello, World!" -> "hello-world"
}

pub fn main(init: std.process.Init) !void {
    return zigbase.App(.{
        .hooks = .{ .posts = .{ .beforeCreate = slugify } },
    }).runCli(init);
}

Using ZigBase in your own project

Add the dependency:

zig fetch --save git+https://github.com/valthon/zigbase

Wire the module into your build.zig:

const zigbase = @import("zigbase");
const zb = b.dependency("zigbase", .{ .target = target, .optimize = optimize });
zigbase.addTo(zb, exe_mod);

addTo adds the import and sets .link_libc = true (SQLite needs libc), so the two can’t drift apart.

Frontend (Zigapagos frontend)

frontend/ is a Zigapagos site with targeted Preact islands: a public post list/detail and a login + “write a post” island that exercises the slugify hook. The example’s comptime .collections schema (users + posts) provisions itself on startup, so the whole thing works from a fresh data directory.

The blog example's authenticated 'Write a post' editor: a logged-in 'New post' form with title and body fields and a Publish button — leaving the slug out lets the server's beforeCreate hook derive it from the title
The blog example’s authenticated ‘Write a post’ editor: a logged-in ‘New post’ form with title and body fields and a Publish button — leaving the slug out lets the server’s beforeCreate hook derive it from the title

cd frontend && ./build.sh && cd ..
zig build
# --insecure-cookies: local dev is over plain HTTP, and auth cookies are Secure by default.
# A strong JWT secret is auto-generated and persisted under the data dir on first run.
./zig-out/bin/blog serve --insecure-cookies --data-dir ./data --serve-static frontend/dist
# open http://127.0.0.1:8090/  — same-origin frontend, so no --realtime-origins needed

This demonstrates ZigBase’s default static-files mode: the binary serves frontend/dist at the root path because you passed --serve-static. The other modes (comptime-hardcoded dir, fully embedded) are shown by the golfsim and plugins examples.

Building and running

This example needs Zig 0.16, which you can get via mise (mise exec zig@0.16.0 -- zig ...). From examples/blog/:

cd frontend && ./build.sh && cd ..
zig build       # produces ./zig-out/bin/blog
./zig-out/bin/blog superuser create --email you@example.com --password <pw> --data-dir ./data
# --insecure-cookies: local dev is over plain HTTP, and auth cookies are Secure by default.
# A strong JWT secret is auto-generated and persisted under the data dir on first run.
./zig-out/bin/blog serve --insecure-cookies --data-dir ./data --serve-static frontend/dist

Then create a posts record without a slug and the hook fills it in from the title.


View source on GitHub →