stk/

Generation is free.
Verification is the product.

Every framework is racing to help an agent write your app. stk is built around the other half: knowing whether what came out still works. Five commands, one loop, and a gate that measures the app in a real browser instead of trusting it.

stk doctor am I ready stk new scaffold stk verify --watch instant feedback stk shell real data stk smoke prove it
1.0

One screen tells you if you can start

stk doctor answers the nine questions you would otherwise discover one failed command at a time. Every line that is not a tick carries the command that fixes it.

$ stk doctor
✓ env file .env
✓ secrets SECRET_KEY and password salt look random
✓ database sqlite stk.db (92 KiB)
✗ migrations database is stamped at 20260715_0002, which no migration defines
→ stk db stamp 228ac4e0ebf5 (after checking the schema matches)
✓ admin user 1 admin(s)
✓ assets vue 3.5.40 vuetify 4.1.6
✓ agent login disabled
✓ browser chromium ready for stk smoke
✓ shell ptpython: highlighting and completion

Real output. That failing line was a genuine problem in the repo, found by the command on its first run.

2.0

Scaffolding ends at a page you can click

Not a four-step checklist. stk new writes the model, views, and template as an extension package inside your app, adds it to the app's extensions, generates the migration, applies it, and hands you the URL. Not one stk file changes.

$ stk new invoice
Scaffolded module: invoice
+ created acme/invoice/__init__.py
+ created acme/invoice/models.py
+ created acme/invoice/views.py
+ created acme/invoice/templates/invoice/index.html
+ added invoice to the extensions in acme/app.py
+ migration generated and applied for invoice
Open: http://localhost:5000/invoices/ (start it with: stk run)

--no-migrate if you would rather write the migration by hand.

3.0

The gate runs while you type

Leave stk verify --watch in a split pane. It re-runs only what your change can break: touching a model re-checks migration drift, touching a template does not. Every failure prints the command that fixes it.

$ stk verify # an unused import, a column with no migration
✗ ruff
Found 1 error.
→ uv run ruff check --fix . && uv run ruff format .
✓ checks
✗ migration-drift
Migration drift detected:
add_column: invoice due_date
→ uv run stk db revision -m "describe change" && uv run stk db upgrade

Migration drift is measured, not guessed. stk runs your migrations onto a throwaway database and diffs the result against your models, so the answer never depends on the state of your own machine. It runs in CI too, which means a model edited without a migration cannot merge.

Remedies land in --json as well, so an agent reads the fix instead of inventing one.

4.0

Ask the running app, not the source

stk shell is an async REPL with the app, a live database session and every model already in scope. Top-level await works, so code copied out of a view runs unchanged.

$ stk shell
models: Activity, OAuth, RateLimitWindow, Role, Session, User, WebAuthn
>>> await count(User)
1
>>> u = await first(User, email="[email protected]")
>>> u.email, u.active
>>> await sql("select count(*) from user_sessions where is_active = 1")
[(3,)]
$ stk shell -c "await count(User)" # one-shot
1
$ stk inspect routes --json # auth per route, condensed
GET,POST /change auth security.change_password
GET,POST /tf-setup auth security.two_factor_setup
GET,POST /wan-register auth security.wan_register
GET /dashboard/ auth portal.dashboard
GET /login open security.login

Auth is read from the real guard chain, not guessed from URL prefixes, and a sanity check fires a genuine anonymous request at every route the report calls guarded. When that check was written it found seven security routes, including 2FA setup and passkey registration, reported as public.

5.0

Prove it in a browser before you ship

stk smoke boots a throwaway app on a free port, logs in through a development-only token route and drives Chromium across the app. It fails on any 4xx or 5xx response, a console error, a failed request, or text that does not contrast with its background.

$ stk smoke
Smoke status: passed
PASSED home /
PASSED login /login
PASSED agent-login /_test/login?token=<redacted>
PASSED dashboard /dashboard/
PASSED admin-users /users/
PASSED /invoices/ /invoices/ # found on its own: every page the app registers
# when it fails, it says what a console error never would:
FAILED home /
problem: invisible text: "GO TO DASHBOARD" contrast 1.0:1

One command, every page it can reach, a screenshot written to .stk/smoke/, and the same command runs in CI on every push.

What the gate has caught

Four real defects in stk itself, all invisible to a passing test suite and a clean console. This is the argument for the whole approach, so the list is specific rather than impressive.

route auth check
Seven security routes reported as public while actually requiring auth

/change, /tf-setup, /wan-register and friends. An agent asking "what is exposed?" was pointed straight at 2FA and passkey management.

contrast check
A primary button rendered invisible at 1.0:1

Vuetify 4 ships its CSS in cascade layers, so an unlayered link colour won over the button's text colour. No console error, no failed request, nothing for a test to catch.

real-browser check
The dashboard hung forever on a production Vue build

Nav filtering mutated state inside a computed. Vue's development build only warns; the production reactivity loops. The app had been shipping the development build, which hid it.

page sweep
GET /mf-recovery returned 500 to anonymous users

A recovery-code template asked for a form variable the library passes under a different name. A security route, unauthenticated, broken in production.

The rule behind the list: every failure becomes a check that cannot pass silently again. All four now have one.

Underneath the tooling

your own auth
Registration, login, sessions, TOTP 2FA, WebAuthn passkeys, recovery codes, OAuth. No per-user billing, no third party holding your users.
no build step
Vue 3 and Vuetify 4 load straight from static files. No node_modules, no bundler. Versions pinned in VERSIONS.txt and refreshed by one script.
one database
SQLite by default, PostgreSQL when you want it. Async SQLAlchemy throughout. Redis optional, Celery absent.
one box
A deploy script to bare metal. No platform required, no vendor between you and your app.
one CLI
Everything under stk, grouped by purpose in stk --help. Every command finds your app from [tool.stk] in pyproject.toml, so there is no environment variable to remember.