Skip to main content

Lightweight full-stack async Python web framework built on Starlette designed for rapid development without compromising in UX

Project description

Z8ter

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.


Features

  • File-based Routing — Views under endpoints/views/ map to routes automatically
  • SSR + Islands — Server-side rendering by default, with React "islands" for interactivity
  • 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 — CSRF protection, rate limiting, security headers out of the box
  • 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 — Production-ready Dockerfile and docker-compose included
  • Health Checks — Built-in /health endpoint for container orchestration
  • Composable BuilderAppBuilder wires config, templating, Vite, sessions, and auth in order
  • CLI Tooling — Scaffold projects, pages, APIs, and manage databases with z8 commands
  • Modern Frontend — Vite, React, TypeScript, Tailwind CSS, and DaisyUI ready out of the box

Quickstart

# Scaffold a new app without installing anything globally
uvx --from z8ter z8 new myapp
cd myapp

# Initialize database
z8 db init

# Install dependencies
uv sync              # Install Python dependencies
npm install          # Install Node dependencies

# Start the dev server
uv run z8 run dev
Alternative: Using pip instead of uv
# Install the CLI into the active virtualenv first
pip install z8ter
z8 new myapp
cd myapp
python3 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
npm install
z8 db init
z8 run dev

Visit http://localhost:8000 to see your app.


Installation

# 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]

Project Structure

myapp/
├── main.py                 # Application entry point
├── .env                    # Environment configuration
├── Dockerfile              # Production container
├── docker-compose.yml      # Local development setup
├── data/                   # SQLite database (auto-created)
│   └── 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/React code
    ├── app.ts
    └── ui-components/      # React Web Components

Application Setup

Basic Setup

# 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 uvicorn

With Authentication and Database

from z8ter.builders.app_builder import AppBuilder
from z8ter.database import Database, SQLiteSessionRepo, SQLiteUserRepo, init_database

# Initialize database
db = init_database()

# Create repositories
session_repo = SQLiteSessionRepo(db, secret_key=os.getenv("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)

Database

Z8ter includes SQLite support with built-in session and user repositories.

Initialize Database

# Using CLI
z8 db init

# Or in Python
from z8ter.database import init_database
db = init_database()

Using Repositories

from z8ter.database import Database, SQLiteUserRepo, SQLiteSessionRepo
from z8ter.auth.crypto import hash_password, verify_password

db = Database()  # Uses DATABASE_URL env or sqlite:///data/app.db

# 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",
)

Database CLI Commands

z8 db init              # Initialize tables
z8 db status            # Show database info
z8 db reset --force     # Drop and recreate tables (destructive!)

Security

Z8ter includes comprehensive security middleware.

CSRF Protection

builder.use_csrf()  # Enable CSRF middleware

In templates:

<form method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <!-- form fields -->
</form>

Rate Limiting

builder.use_rate_limiting(
    requests_per_minute=60,
    burst_size=10,
    exempt_paths=["/health", "/static"],
)

Security Headers

builder.use_security_headers(
    enable_hsts=True,  # Only in production with HTTPS
    x_frame_options="DENY",
    content_security_policy="default-src 'self'",
)

Input Validation

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")

Account Lockout

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)

Password Reset & Email Verification Tokens

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 None

See docs/security.md for the full security guide, including .env handling and dependency auditing with pip-audit.


Email

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.


Background Tasks

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 (session_repo.cleanup_expired()) automatically. In handlers, spawn background work without delaying the response:

request.app.state.task_manager.spawn(send_welcome_email, user["email"])

See docs/background-tasks.md.


Testing

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.


Creating Pages

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")

Creating APIs

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}})

Protected Routes

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})

Deployment

Docker

# Build and run
docker build -t myapp .
docker run -p 8000:8000 -e Z8TER_DEBUG=false myapp

# Or with docker-compose
docker compose up

Environment Variables

Variable Description Default
Z8TER_DEBUG Enable debug mode false
DATABASE_URL SQLite database URL sqlite:///data/app.db
APP_SESSION_KEY Secret key for sessions (32+ chars) Required
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

Health Check

The /health endpoint returns:

{"status": "healthy", "version": "0.3.0"}

Module Overview

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

CLI Commands

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)

Configuration Reference

AppBuilder Methods

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

Requirements

  • Python 3.10+
  • Node.js 18+ (for frontend tooling)

License

MIT License — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

z8ter-0.3.0.tar.gz (227.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

z8ter-0.3.0-py3-none-any.whl (253.2 kB view details)

Uploaded Python 3

File details

Details for the file z8ter-0.3.0.tar.gz.

File metadata

  • Download URL: z8ter-0.3.0.tar.gz
  • Upload date:
  • Size: 227.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for z8ter-0.3.0.tar.gz
Algorithm Hash digest
SHA256 229f2102f5ff8bb2bcc56c7550d3d0c114ebcfa32bf4bf54a164b37e441a76be
MD5 810eda6250b1a2e1efabe77e4e01a1d6
BLAKE2b-256 de86f8027ccf9f822889b1327d4c496f6eccef3316e4c88ac0ed37e2b873bac6

See more details on using hashes here.

File details

Details for the file z8ter-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: z8ter-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 253.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for z8ter-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 da6ece5dfc4e82d5f8cdc697fb0adb07388cb80e685e8d4ab3d7a9e50d19ee54
MD5 a6de7a7279d859e3f4572987b72e10d5
BLAKE2b-256 2f8de554390ec3ff603aed5bfa98b62b45bbcd2a68835259a9172c37f9f97d57

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page