Command line tool
spider creates a project, runs it while you work, and generates code inside it. This page goes through every command, with what it prints.
spider new <name> create a project
spider dev build, run, reload after every save
spider g feature <name> generate a feature
spider g auth add a login
spider migrate apply the migrations (the modules', then the app's)
spider routes list the routes and who may call them
spider check check the conventions (--modules: the modules)
spider install download the frontend assets
spider ui show or switch the UI kit
spider icons show, add or remove icon sets
spider add pwa make the app installable
spider remove pwa undo that
spider generate-vapid make the keys for Web Push
spider update move the project to the latest Spider
spider self-update update the tool itself
spider version the version installed
Every command explains itself: spider help new, or spider new --help.
Install
curl -fsSL https://raw.githubusercontent.com/llllOllOOll/spider/main/scripts/install.sh | bash
spider version prints the version installed:
spider v0.11.0
spider self-update downloads and installs the latest one.
spider new
spider new myapp
It writes the project to ./myapp, one line per file, fetches Spider, and says what to do next:
create myapp/src/features/home/controller.zig
create myapp/src/features/home/routes.zig
create myapp/src/features/home/routes_test.zig
create myapp/Dockerfile
create myapp/.dockerignore
create myapp/.env.example
create myapp/.gitignore
create myapp/src/app_test.zig
create myapp/src/ui.css (UI kit: daisyui)
create myapp/.env (local settings, git-ignored; JWT_SECRET generated)
create myapp/AGENTS.md
create myapp/CLAUDE.md
create myapp/public/images/logo.png
create myapp/public/favicon.png
create myapp/public/favicon.ico
create myapp/src/core/db/migrations.zig
create myapp/src/core/db/mod.zig
update myapp/src/core/mod.zig
CSS will be compiled on first 'zig build run'
Fetching Spider v0.11.0...
info: resolved to commit 4deaac0c713a78d801763cc072c9b48182447625
Done! Next steps:
cd myapp
spider dev ← builds, runs, and reloads the browser as you edit
zig build run ← or: one build, one run
(requires spider CLI in PATH — run: spider install)
Without options the project has HTML views, a SQLite database and a first test. The options change that:
--pguses PostgreSQL instead of SQLite. Its migrations are built into the binary, andmainapplies them before it serves (Migrations in production); it also gets adocker-compose.ymlthat runs PostgreSQL for development.--no-dbleaves the database out.--apimakes a JSON project, without HTML views.--ui=tailwinduses plain Tailwind instead of daisyUI.spider ui useswitches later.--htmx=4uses htmx 4 instead of htmx 2. The generators then write what that version understands.--pwamakes the app installable. It is the same as runningspider add pwaafterwards.--skip-downloadsdoes not download Tailwind, Alpine, htmx and the icons now.spider installdoes it later.
What the project has:
build.zig build.zig.zon the build, and Spider as a dependency
spider.config.zig settings that are the same everywhere
.env .env.example settings of this machine; .env stays out of git
Dockerfile
src/main.zig starts the server
src/app_test.zig tests that call the running app
src/features/ one folder per feature; home/ is the first
src/features/mod.zig the list of features the server mounts
src/core/db/ the database and its migrations
src/shared/templates/ the layout and what every page shares
src/styles.css src/ui.css the styles, and the classes of the UI kit
public/ files served as they are
spider dev
Run it inside the project.
spider dev
It builds the app, runs it, and after every save builds it again and reloads the browser. While a build runs or after it fails, the version that was up keeps answering, and the compiler's errors go to the terminal. Ctrl+C stops it.
--port 4000 makes the app listen on another port for this run. Development has more on working this way.
spider g feature
spider g feature notes
A feature is a folder with everything one part of the app needs. The command writes it, with pages to list, create, edit and delete:
Generating feature 'notes'...
create src/features/notes/mod.zig
create src/features/notes/model.zig
create src/features/notes/repository.zig
create src/features/notes/controller.zig
create src/features/notes/routes.zig
create src/features/notes/routes_test.zig
create src/features/notes/views/index.html
create src/features/notes/views/NotesList.html
create src/features/notes/views/NotesCard.html
create src/features/notes/views/NotesForm.html
create src/features/notes/views/NotesEditForm.html
create src/features/notes/views/page.html
create src/core/db/migrations/1792419124_create_notes.sql
update src/core/db/migrations.zig
update src/features/mod.zig
(routes mounted by mountFeatures in src/main.zig)
Done! Next:
spider migrate create the notes table (until then /notes answers 500)
spider routes check what got registered, and who may call it
access: src/features/notes/routes.zig (defaults() and each route's config)
Nothing in src/main.zig changes: the server mounts every feature listed in src/features/mod.zig, and the command adds the new one there.
In a project on PostgreSQL there is no list of migrations to update: the line says so instead, (built into the binary by spider.addMigrations: no list to update).
--api generates JSON routes instead of pages.
The feature starts with one column, name. Add yours to the migration, to the struct in model.zig and to the forms.
spider g auth
spider g auth
It adds a login with the app's own accounts: a users table, pages to sign up, sign in and sign out, and their tests.
Generating login with the app's own users...
create src/features/auth/mod.zig
create src/features/auth/model.zig
create src/features/auth/repository.zig
create src/features/auth/controller.zig
create src/features/auth/routes.zig
create src/features/auth/routes_test.zig
create src/features/auth/views/login.html
create src/features/auth/views/register.html
create src/features/auth/views/account.html
update src/features/mod.zig
create src/core/db/migrations/1792419128_create_users.sql
update src/core/db/migrations.zig
update src/main.zig (session middleware)
update src/app_test.zig (tests of the login)
Done. Open /auth/register to create the first account.
Every route that is not `.public` now needs a login. Features made before
this have their access undeclared: `spider check` lists them.
Logins are signed with JWT_SECRET (.env). The app applies the new
migration when it starts.
--apigenerates JSON routes that answer a token, without pages.--provider=keycloakgenerates a login through a Keycloak realm instead.
Authentication explains what was generated and how to use it.
spider migrate
A migration is a file in src/core/db/migrations/, named with a number and what it does. It has two parts: what to apply, and how to undo it.
-- migrate:up
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
inserted_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- migrate:down
DROP TABLE IF EXISTS notes;
spider migrate applies the up part of each one that has not run yet, in order:
Running migrations on SQLite: db.sqlite...
running 1792419124_create_notes
applied 1792419124_create_notes
running 1792419128_create_users
applied 1792419128_create_users
2 migration(s) applied.
Run it again and there is nothing to do:
Running migrations on SQLite: db.sqlite...
Nothing to migrate.
It reads where the database is from .env: SQLITE_PATH for SQLite, PG_HOST, PG_PORT, PG_USER, PG_PASSWORD and PG_DB for PostgreSQL. The app applies pending migrations when it starts too, so the command is for when you want them applied without running the app. Database is about writing them.
A project made with --pg has its migrations in its binary. There the command builds the app and runs it with SPIDER_MIGRATE set, and the app applies them itself:
Running migrations (zig build run with SPIDER_MIGRATE)...
info: pg: connected (10 connections)
info: migration 1792419170_create_notes applied
info: 1 migration(s) applied
A server does the same with the binary alone, SPIDER_MIGRATE=1 ./myapp: Migrations in production.
When the app mounts modules, their migrations run first, then the app's. In a project that keeps its migrations as files, --no-modules runs only the app's and --modules-only only the modules'.
spider routes
spider routes
It builds the app, starts it up to the point where it would listen, and prints every route: the method, the path, and who may call it.
GET / public
GET /_spider/health public quiet_log
GET /auth/account authenticated
GET /auth/login public
POST /auth/login public
POST /auth/logout authenticated
GET /auth/register public
POST /auth/register public
GET /notes -
POST /notes/:id/delete -
GET /notes/:id/edit -
POST /notes/:id/update -
POST /notes/create -
GET /notes/new -
GET /up public quiet_log
15 routes
The third column is the access the route declares: public, authenticated, roles:, org:, policy:, or - when it declares none. In an app with a login, - means any signed-in visitor may call it.
--check is for that case. It fails when the app has a login and a route declares nothing:
6 route(s) declare no access; with auth, any logged-in user may call them:
GET /notes
POST /notes/:id/delete
GET /notes/:id/edit
POST /notes/:id/update
POST /notes/create
GET /notes/new
fix: .roles / .org_roles / .authenticated / .policy / .public in the route's config, or defaults() in its group
--lock writes the listing to routes.lock. Commit it.
wrote routes.lock (15 routes)
--diff compares the app with that file and fails when they differ, so a change of access shows up in review and in CI:
routes differ from routes.lock:
- GET /notes/new -
+ GET /notes/new authenticated
if intended, review and update it: spider routes --lock
--json prints the listing as one line of JSON.
spider check
spider check
It reads the project and reports what does not follow its conventions, each with the file, the line and the fix:
src/features/notes/routes.zig:21: error [route-access]: route declares no access; with auth, any logged-in user may call it
fix: add .roles / .org_roles / .authenticated (any logged-in user) / .policy or .public to its config, or a defaults(...) to its group
6 error(s), 0 warning(s)
What it treats as an error:
- a route registered outside a feature's
routes.zig; - a class of the UI kit (
btn,card) written in a template instead of itsui-class; - an icon class of a set that
src/styles.cssdoes not load; - a feature that is not in
src/features/mod.zig; - in an app with a login, a route that declares no access.
And as a warning: an inline style=, a pasted <svg>, a feature without routes_test.zig. --strict fails on warnings too, which is what you want in CI.
A new project runs the same checks in zig build test, so they also fail there. Testing shows that.
--modules also builds the app and runs it with SPIDER_CHECK set: every module it mounts runs the checks listen() runs before it starts (its variables, its migrations, its own check), and the report is printed. It exits with 1 when one is not ready:
spider: 1 module(s) not ready, refusing to start
heartbeat: HEARTBEAT_TOKEN is required but not set
It reads the database and the environment as the app does. Modules has more.
spider install
spider install
It downloads the frontend assets into the project: Tailwind, Alpine, htmx, daisyUI when the UI kit uses it, and the icon sets that src/styles.css loads. A new project has them already. They are not committed to git, so run it after cloning a project.
spider ui
Templates use classes that start with ui- (ui-btn, ui-input, ui-card). src/ui.css says what each one means, and it is the only file that knows which UI kit the app uses.
spider ui shows the kit in use and the ones available:
UI kit: daisyui (src/ui.css)
Kits:
daisyui daisyUI 5 components and themes (bin/daisyui.mjs)
tailwind plain Tailwind, no component library
Switch: spider ui use <kit>
spider ui use tailwind rewrites src/ui.css for that kit and lists the template lines that still use the old kit's classes directly. It refuses when you have edited src/ui.css, unless you add --force.
spider icons
An icon is a class: <span class="hero-home size-5"></span>. spider icons shows the sets the app loads:
Icon sets in this app (src/styles.css):
heroicons hero-* e.g. hero-home, hero-home-solid, hero-home-mini, hero-home-micro
Available: heroicons, lucide, tabler
Add or remove one: spider icons add <set> / spider icons remove <set>
spider icons add lucide adds the set to src/styles.css and downloads it. spider icons remove lucide takes it out and lists the template lines that still use its classes.
spider add pwa
spider add pwa
It makes the app installable on a phone or a desktop: a manifest, a service worker that shows a page when the device is offline, the script that registers it, and the icons.
create public/manifest.webmanifest
create public/sw.js
create public/offline.html
create public/js/pwa.js
create public/pwa/icon-192.png
create public/pwa/icon-512.png
create public/pwa/icon-maskable-512.png
create public/pwa/apple-touch-icon.png
update src/shared/templates/layout.html (<head>: manifest, theme-color, pwa.js)
update src/shared/templates/app.html (<head>: manifest, theme-color, pwa.js)
PWA added. Check it in Chrome DevTools > Application > Manifest / Service workers.
- Replace public/pwa/*.png with your icons (the maskable one keeps the logo
inside the center 80%), and set name/colors in public/manifest.webmanifest.
- Install button: <button x-show="$store.pwa.canInstall" @click="$store.pwa.install()">Install</button>
- Service workers need HTTPS (localhost is exempt).
The service worker never keeps pages or API answers: what the visitor sees is always what the server sent.
spider remove pwa undoes it. It deletes the manifest, the offline page, the script and the icons, and replaces public/sw.js with one that removes itself from the browsers that installed the app:
update public/sw.js (unregisters itself in browsers that installed the app)
delete public/manifest.webmanifest
delete public/offline.html
delete public/js/pwa.js
delete public/pwa/
update src/shared/templates/layout.html (removed the PWA <head> block)
update src/shared/templates/app.html (removed the PWA <head> block)
Keep public/sw.js for a few weeks — until your users have visited again —
then delete it.
spider generate-vapid
Web Push needs a pair of keys that identifies the app to the push services. This makes one:
spider generate-vapid mailto:ops@example.com
VAPID keys generated successfully!
Add these to your .env file:
VAPID_SUBJECT=mailto:ops@example.com
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
The argument is how a push service can reach you: a mailto: or an https: address. Generate the pair once and keep it. Subscriptions are tied to the public key, so a new pair loses every subscriber.
spider update
spider update moves the project to the latest Spider. It changes build.zig.zon, so read the changelog before you run it: a release may change what the app has to do.