Z8ter is a lightweight, async Python web framework built on Starlette, designed for rapid development without compromising UX. It combines SSR-first rendering, auto-discovered routes, pluggable authentication, SQLite persistence, security middleware, and CLI tooling into one cohesive developer experience.
Status: Public Alpha — Z8ter is under active development and not yet recommended for production. APIs may change without notice.
- File-based Routing — Views under
endpoints/views/map to routes automatically - SSR + Islands — Server-side rendering by default, with optional custom-element islands; the generated starter uses Solid
- Decorator-driven APIs — Define REST APIs using decorators; auto-mounted under
/api/<name> - Pluggable Auth — Session middleware, Argon2 password hashing, and route guards like
@login_required - SQLite Database — Built-in SQLite persistence with session and user repositories
- Security Middleware — Opt-in CSRF protection and rate limiting; the starter enables security headers
- Account Protection — Account lockout plus signed password-reset and email-verification tokens
- Transactional Email — Pluggable providers (console, SMTP) with async sending and Jinja templates
- Background Tasks — In-process asyncio task manager with interval scheduling and session cleanup
- Testing Utilities — In-memory repos and an email outbox for fast, dependency-free app tests
- Docker Ready — Multi-stage Dockerfile and Compose configuration included
- Health Checks — Built-in
/healthendpoint for container orchestration - Composable Builder —
AppBuilderwires config, templating, Vite, sessions, and auth in order - CLI Tooling — Scaffold projects, pages, APIs, and manage databases with
z8commands - Modern Frontend — Vite 7, Solid, TypeScript, Tailwind CSS 4, and DaisyUI 5 in the generated starter
Version 0.3.1 fixes CSRF form handling, updates the generated starter, and refreshes dependencies. It requires Starlette 1.6 or newer (below 2.0), where PyPI 0.3.0 supported Starlette below 1.0. Review your Starlette extensions and dependency pins when upgrading. See release notes.
# Scaffold a new app without installing anything globally
uvx --from z8ter z8 new myapp
cd myapp
# Install dependencies
uv sync # Install Python dependencies
npm install # Install Node dependencies
# Build assets, then start frontend watchers and the Python server
npm run build
uv run npm run devAlternative: Using pip instead of uv
# Create and activate an environment before installing the CLI
python3 -m venv .z8ter-env
source .z8ter-env/bin/activate # Windows: .z8ter-env\Scripts\Activate.ps1
python -m pip install z8ter
z8 new myapp
cd myapp
python -m pip install -r requirements.txt
npm install
npm run build
npm run devVisit http://localhost:8000 to see your app. Database setup is optional for the basic starter. The frontend watchers rebuild assets; reload the browser after frontend changes.
# One-off CLI usage
uvx --from z8ter z8 --help
# Persistent CLI install
uv tool install z8ter
# Or inside an existing project environment
uv add z8ter
# Or using pip
pip install z8ter
# For development
pip install "z8ter[dev]"myapp/
├── main.py # Application entry point
├── .env # Environment configuration
├── Dockerfile # Production container
├── docker-compose.yml # Local development setup
├── data/ # Optional application data directory
│ └── app.db
├── endpoints/
│ ├── views/ # SSR page views → URLs
│ │ ├── index.py # → /
│ │ └── about.py # → /about
│ └── api/ # REST API endpoints
│ └── hello.py # → /api/hello/*
├── templates/ # Jinja2 templates
│ ├── base.jinja
│ └── pages/
├── content/ # YAML page content
├── static/ # Static assets
└── src/ts/ # TypeScript/Solid code
├── app.ts
└── ui-components/ # Solid custom elements
# main.py
import os
from z8ter.builders.app_builder import AppBuilder
DEBUG = os.getenv("Z8TER_DEBUG", "true").lower() == "true"
builder = AppBuilder()
builder.use_config(".env")
builder.use_templating()
builder.use_vite()
builder.use_errors()
builder.use_security_headers()
builder.use_health_check()
app = builder.build(debug=DEBUG)
asgi_app = app.starlette_app # For uvicornfrom z8ter.builders.app_builder import AppBuilder
from pathlib import Path
from z8ter.config import build_config
from z8ter.database import SQLiteSessionRepo, SQLiteUserRepo, init_database
config = build_config(".env")
db_path = Path("data/app.db").resolve()
db = init_database(url=f"sqlite:///{db_path.as_posix()}")
# APP_SESSION_KEY must be configured before enabling sessions.
session_repo = SQLiteSessionRepo(db, secret_key=config("APP_SESSION_KEY"))
user_repo = SQLiteUserRepo(db)
builder = AppBuilder()
builder.use_config(".env")
builder.use_templating()
builder.use_vite()
builder.use_auth_repos(session_repo=session_repo, user_repo=user_repo)
builder.use_authentication()
builder.use_app_sessions()
builder.use_csrf() # CSRF protection
builder.use_rate_limiting() # Rate limiting
builder.use_security_headers()
builder.use_health_check()
builder.use_errors()
app = builder.build(debug=False)Z8ter includes SQLite support with built-in session and user repositories.
Choose an explicit absolute path for SQLite. The current default URL resolves to /data/app.db, not a project-relative path.
# Set DATABASE_URL in the process environment when using the database CLI.
DATABASE_URL="sqlite:///$(pwd)/data/app.db" z8 db initfrom pathlib import Path
from z8ter.database import init_database
db_path = Path("data/app.db").resolve()
db = init_database(url=f"sqlite:///{db_path.as_posix()}")from datetime import datetime, timedelta, timezone
from pathlib import Path
from z8ter.database import SQLiteUserRepo, SQLiteSessionRepo, init_database
from z8ter.auth.crypto import hash_password
db_path = Path("data/app.db").resolve()
db = init_database(url=f"sqlite:///{db_path.as_posix()}")
# User operations
user_repo = SQLiteUserRepo(db)
user = user_repo.create_user(
email="user@example.com",
password_hash=hash_password("secret123"),
name="John Doe",
)
# Session operations
session_repo = SQLiteSessionRepo(db, secret_key="your-secret-key")
session_repo.insert(
sid_plain="session-id",
user_id=user["id"],
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
remember=True,
ip="127.0.0.1",
user_agent="Mozilla/5.0",
)z8 db init # Initialize tables
z8 db status # Show database info
z8 db reset --force # Drop and recreate tables (destructive!)Z8ter includes comprehensive security middleware.
builder.use_csrf() # Enable CSRF middleware; secure cookies require HTTPS
# Local HTTP development only: builder.use_csrf(cookie_secure=False)Form-token submissions preserve fields and uploaded files for the handler. They
are buffered up to 8 MiB by default and return HTTP 413 above that limit. Set
max_form_body_size to change it. Send X-CSRF-Token for larger uploads that
should stream without CSRF buffering; application and proxy limits still apply.
In templates:
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- form fields -->
</form>builder.use_rate_limiting(
requests_per_minute=60,
burst_size=10,
exempt_paths=["/health", "/static"],
)builder.use_security_headers(
enable_hsts=True, # Only in production with HTTPS
x_frame_options="DENY",
content_security_policy="default-src 'self'",
)from z8ter.security.validators import validate_email, validate_password
errors = []
if not validate_email(email):
errors.append("Invalid email format")
if not validate_password(password, min_length=8):
errors.append("Password must be at least 8 characters")Protect accounts from password guessing (complements per-IP rate limiting):
from z8ter.security import AccountLockout
lockout = AccountLockout(max_attempts=5, lockout_seconds=900)
if lockout.is_locked(email):
return error_response() # indistinguishable from a bad password
if verify_password(stored_hash, password):
lockout.record_success(email)
else:
lockout.record_failure(email, ip_address=client_ip)Stateless, signed, time-limited tokens — no database table required:
from z8ter.auth.tokens import TokenManager
tokens = TokenManager(config("APP_SESSION_KEY"))
token = tokens.generate_password_reset_token(user_id)
user_id = tokens.verify_password_reset_token(token) # None if invalid/expired
token = tokens.generate_email_verification_token(user_id, email)
data = tokens.verify_email_verification_token(token) # {"uid", "email"} or NoneSee docs/security.md for the full security guide,
including .env handling and dependency auditing with pip-audit.
Enable transactional email with a provider resolved from config:
builder.use_email() # EMAIL_PROVIDER=console (default) or smtp# In a handler
email = request.app.state.email
await email.send_email(
to="user@example.com",
subject="Welcome!",
text="Thanks for signing up.",
)
# Or render from your Jinja templates
await email.send_template(
to="user@example.com",
subject="Welcome!",
template="emails/welcome.html",
context={"name": "Ada"},
)The console provider logs messages during development; configure SMTP for
production via SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD.
See docs/email.md.
Run recurring and fire-and-forget work in-process — no broker required:
from z8ter.tasks import TaskManager
tasks = TaskManager()
@tasks.interval(seconds=300)
async def refresh_cache():
...
builder.use_background_tasks(task_manager=tasks)use_background_tasks() also schedules hourly session cleanup when a registered
session repository provides cleanup_expired(). Tasks run in the web process and
are not durable across restarts. In handlers, spawn
background work without delaying the response:
request.app.state.task_manager.spawn(send_welcome_email, user["email"])Test apps without a database or mail server using z8ter.testing:
from starlette.testclient import TestClient
from z8ter.testing import InMemorySessionRepo, InMemoryUserRepo, create_test_app
def test_login_flow():
app = create_test_app(
session_repo=InMemorySessionRepo(),
user_repo=InMemoryUserRepo(),
)
client = TestClient(app.starlette_app)
...Capture outbound email with InMemoryEmailProvider and assert on its
outbox. See docs/testing.md.
z8 create_page products# endpoints/views/products.py
from z8ter.endpoints.view import View
from z8ter.requests import Request
from z8ter.responses import Response
class Products(View):
async def get(self, request: Request) -> Response:
return self.render(request, "pages/products.jinja")z8 create_api users# endpoints/api/users.py
from z8ter.endpoints.api import API
from z8ter.requests import Request
from z8ter.responses import JSONResponse
class Users(API):
@API.endpoint("GET", "/")
async def list_users(self, request: Request):
return JSONResponse({"ok": True, "users": []})
@API.endpoint("GET", "/{user_id:int}")
async def get_user(self, request: Request):
user_id = request.path_params["user_id"]
return JSONResponse({"ok": True, "user": {"id": user_id}})from z8ter.endpoints.view import View
from z8ter.auth.guards import login_required
class Dashboard(View):
@login_required
async def get(self, request):
user = request.state.user
return self.render(request, "pages/dashboard.jinja", {"user": user})# Build and run
docker build -t myapp .
docker run -p 8000:8000 -e Z8TER_DEBUG=false myapp
The bundled Compose file is a development example that mounts the host source
over the image's assets. Build assets on the host before using it. Its optional
dev profile needs additional frontend/Python setup; use the documented local
watchers or the Docker image commands above instead. Container execution was
not verified as part of the 0.3.1 update.
| Variable | Description | Default |
|---|---|---|
Z8TER_DEBUG |
Enable debug mode | false |
DATABASE_URL |
SQLite URL; pass a configured absolute path for project data | sqlite:///data/app.db (resolves to /data/app.db) |
APP_SESSION_KEY |
Secret key for sessions (32+ chars) | Required when enabling session/auth features |
VITE_DEV_SERVER |
Vite dev server URL (dev only) | - |
EMAIL_PROVIDER |
Email provider: console or smtp |
console |
EMAIL_FROM |
Default sender address | - |
SMTP_HOST / SMTP_PORT |
SMTP server (when EMAIL_PROVIDER=smtp) |
- / 587 |
SMTP_USERNAME / SMTP_PASSWORD |
SMTP credentials (optional) | - |
SMTP_USE_TLS / SMTP_USE_SSL |
STARTTLS / implicit SSL | true / false |
When use_health_check() is enabled, /health reports the framework version (it does not probe external services):
{"status": "healthy", "version": "0.3.1"}| Module | Purpose |
|---|---|
z8ter.core |
ASGI wrapper around Starlette |
z8ter.endpoints |
Base View and API classes |
z8ter.builders |
AppBuilder and composable setup steps |
z8ter.auth |
Session management, crypto, guards, middleware, tokens |
z8ter.database |
SQLite persistence, session/user repositories |
z8ter.security |
CSRF, rate limiting, headers, validators, lockout |
z8ter.email |
Transactional email (console/SMTP providers, async service) |
z8ter.tasks |
In-process background task manager |
z8ter.testing |
In-memory repos and test app helpers |
z8ter.route_builders |
Auto-discovery of views and APIs |
z8ter.vite |
Vite asset integration (dev server + manifest) |
z8ter.cli |
CLI commands |
z8ter.config |
Environment-based configuration |
z8ter.errors |
Centralized error handling |
| Command | Description |
|---|---|
z8 new <name> |
Create a new project |
z8 create_page <name> |
Scaffold a page (view + template + content + TS) |
z8 create_api <name> |
Scaffold an API endpoint |
z8 run [dev|prod|LAN|WAN] |
Run the server |
z8 db init |
Initialize database tables |
z8 db status |
Show database status |
z8 db reset |
Reset database (destructive) |
| Method | Description |
|---|---|
use_config(env_file) |
Load environment configuration |
use_templating() |
Initialize Jinja2 templates |
use_vite() |
Enable Vite asset integration |
use_auth_repos(session_repo, user_repo) |
Register auth repositories |
use_authentication() |
Enable auth session middleware |
use_app_sessions() |
Enable application sessions |
use_csrf() |
Enable CSRF protection |
use_rate_limiting() |
Enable rate limiting |
use_security_headers() |
Add security headers |
use_email() |
Enable transactional email service |
use_background_tasks() |
Enable background task manager |
use_health_check() |
Add /health endpoint |
use_errors() |
Register error handlers |
build(debug) |
Build the application |
- Python 3.10+
- Node.js 22.12+ (for frontend tooling)
MIT License — see LICENSE for details.