# Vibetuner > Vibetuner is a production-ready FastAPI project scaffolding tool that generates full-stack web applications with authentication, flexible database support (MongoDB or SQL), frontend, Docker deployment, and CLI tools pre-configured in seconds. Built by All Tuner Labs for rapid iteration and modern development. Important notes: - Vibetuner consists of three packages: a Python framework (`vibetuner`), a JavaScript package (`@alltuner/vibetuner`), and a Copier scaffolding template - The framework separates immutable framework code (`vibetuner` package) from your application code (`src/app/`) for clean updates - In all examples, `app` refers to your project's Python package (the directory under `src/`). The actual name depends on your project slug (e.g., `src/myproject/` for a project named "myproject") - HTMX is used instead of React/Vue for simplicity - server-rendered HTML with sprinkles of interactivity - All tools are chosen for speed: uv (Python), bun (JavaScript), Granian (ASGI server), Ruff (linting) - The project is designed to work excellently with AI coding assistants like Claude, Cursor, and ChatGPT ## Quick Start ### Prerequisites - **Python 3.11+**: [python.org/downloads](https://www.python.org/downloads/) - **Docker**: [docker.com/get-started](https://www.docker.com/get-started/) (for containerized development) That's it! Vibetuner handles the rest. ### Create Your First Project **Using uvx (Recommended)** No installation needed: ```bash uvx vibetuner scaffold new my-app ``` **Install Globally** ```bash uv tool install vibetuner vibetuner scaffold new my-app ``` **Interactive Setup** The scaffold command will ask you: - **Project name**: `my-app` - **Company name**: Your company/organization - **Author details**: Name and email - **Features**: OAuth providers, background jobs, etc. **Skip Prompts** Use defaults for everything: ```bash uvx vibetuner scaffold new my-app --defaults ``` ### Start Development ```bash cd my-app just dev ``` For projects using SQL models (SQLModel/SQLite/PostgreSQL), create tables first: ```bash uv run vibetuner db create-schema # Required once before first run just dev ``` This starts: - Database (MongoDB or SQL, if configured) - Redis (if background jobs enabled) - FastAPI application with hot reload - Frontend asset compilation Visit `http://localhost:8000` - your app is running! ### Project Structure ```text my-app/ ├── src/app/ # Your code (edit freely) │ ├── frontend/routes/ # Your HTTP routes │ ├── models/ # Your database models │ └── services/ # Your business logic ├── templates/ # Jinja2 templates ├── assets/ # Static files └── Dockerfile # Production deployment ``` The `vibetuner` framework (auth, database, core services) is installed as a package dependency. ### Justfile Commands All project management tasks use `just` (command runner). Run `just` to see all available commands. **Development:** ```bash just dev # Docker development with hot reload (recommended) just local-dev PORT=8000 # Local development without Docker just worker-dev # Background worker (if background jobs enabled) ``` **Dependencies:** ```bash just install-deps # Install from lockfiles just update-repo-deps # Update root scaffolding dependencies uv add package-name # Add Python package bun add package-name # Add JavaScript package ``` **Code Formatting:** ```bash just format # Format ALL code (Python, Jinja, TOML, YAML) just format-py # Format Python with ruff just format-jinja # Format Jinja templates with djlint just format-toml # Format TOML files with taplo just format-yaml # Format YAML files with dprint ``` **IMPORTANT**: Always run `ruff format .` or `just format-py` after Python changes. **Code Linting:** ```bash just lint # Lint ALL code just lint-py # Lint Python with ruff just lint-jinja # Lint Jinja templates with djlint just lint-md # Lint markdown files just lint-toml # Lint TOML files with taplo just lint-yaml # Lint YAML files with dprint just type-check # Type check Python with ty ``` **Localization:** ```bash just i18n # Full workflow: extract, update, compile just extract-translations # Extract translatable strings just update-locale-files # Update existing .po files just compile-locales # Compile .po to .mo files just new-locale LANG # Create new language (e.g., just new-locale es) ``` **CI/CD & Deployment:** ```bash just build-dev # Build development Docker image just test-build-prod # Test production build locally just build-prod # Build production image just release # Build and release production image just deploy-latest HOST # Deploy to remote host ``` **Scaffolding:** ```bash just update-scaffolding # Update project to latest vibetuner template ``` ## Core Documentation ### Development Guide #### Development Environment Vibetuner supports two development modes: **Docker Development (Recommended)** Run everything in containers with hot reload: ```bash just dev ``` This starts: - Database (MongoDB or SQL, if configured) - Redis (if background jobs enabled) - FastAPI application with auto-reload - Frontend asset compilation with watch mode Changes to Python code, templates, and assets automatically reload. **Local Development** Run services locally without Docker: ```bash # Terminal 1: Frontend assets bun dev # Terminal 2: Backend server just local-dev ``` A database (MongoDB or SQL) is required if using database features. Redis is only required if background jobs are enabled. **HTTPS via Tailscale Expose** Use `just dev-exposed` to run the dev server with HTTPS via Tailscale Serve. This wraps `just local-all` with automatic tailscale serve setup/teardown. #### Adding New Routes Create a new file in `src/app/frontend/routes/`: ```python # src/app/frontend/routes/blog.py from fastapi import APIRouter router = APIRouter(prefix="/blog", tags=["blog"]) @router.get("/") async def list_posts(): return {"posts": []} ``` Register in `src/app/frontend/__init__.py`: ```python from app.frontend.routes import blog app.include_router(blog.router) ``` **`@render` decorator** — for simple routes, eliminate `render_template()` boilerplate: ```python from vibetuner import render @router.get("/dashboard") @render("dashboard.html.jinja") async def dashboard(request: Request, user=Depends(get_current_user)) -> dict: return {"user": user} ``` Returns `Response` objects unchanged (escape hatch for redirects, conditional logic). #### Adding Database Models Create models in `src/app/models/`. The approach depends on your database choice: **MongoDB (Beanie ODM)** ```python # src/app/models/post.py from beanie import Document from pydantic import Field class Post(Document): title: str content: str published: bool = Field(default=False) class Settings: name = "posts" ``` **SQL (SQLModel)** ```python # src/app/models/post.py from sqlmodel import SQLModel, Field class Post(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) title: str content: str published: bool = Field(default=False) ``` For SQL databases, create tables with: `vibetuner db create-schema` Register models in `src/app/models/__init__.py`: ```python from app.models.post import Post __all__ = ["Post"] ``` The model will be automatically registered with the database on startup. #### Creating Templates Add templates in `templates/`: ```html {% extends "base/skeleton.html.jinja" %} {% block content %}

Blog Posts

{% for post in posts %}

{{ post.title }}

{{ post.content }}
{% endfor %}
{% endblock %} ``` #### Built-in Template Filters Vibetuner provides several built-in template filters for common formatting needs: | Filter | Usage | Output | |--------|-------|--------| | `timeago` | `{{ dt \| timeago }}` | "5 minutes ago" | | `timeago(short=True)` | `{{ dt \| timeago(short=True) }}` | "5m ago" | | `format_date` | `{{ dt \| format_date }}` | "January 15, 2025" | | `format_datetime` | `{{ dt \| format_datetime }}` | "January 15, 2025 at 2:30 PM" | | `format_duration` / `duration` | `{{ seconds \| duration }}` | "5 min" or "30 sec" | The `timeago` filter converts a datetime to a human-readable relative time string. Use `short=True` for compact displays like "5m ago", "1d ago", "3mo ago". Short format outputs: < 60 seconds = "just now", < 60 minutes = "Xm ago", < 24 hours = "Xh ago", < 7 days = "Xd ago", < 30 days = "Xw ago", < 365 days = "Xmo ago", < 4 years = "Xy ago", >= 4 years = "MMM DD, YYYY". #### Adding Custom Template Filters Register custom Jinja2 filters via the `template_filters` dict in `VibetunerApp`: ```python # src/app/frontend/templates.py def uppercase(value): """Convert value to uppercase""" return str(value).upper() def format_money(value): """Format value as USD currency""" try: return f"${float(value):,.2f}" except (ValueError, TypeError): return str(value) ``` ```python # src/app/tune.py from vibetuner import VibetunerApp from app.frontend.templates import uppercase, format_money app = VibetunerApp( template_filters={ "uppercase": uppercase, "money": format_money, }, ) ``` Use in templates: ```html

{{ user.name | uppercase }}

Price: {{ product.price | money }}

``` **Important:** If a filter returns HTML, wrap the result with `markupsafe.Markup` to prevent Jinja2 auto-escaping. Always escape user input inside the markup: ```python from markupsafe import Markup, escape def tag_badge(value: str) -> Markup: """Render a badge — escape user input, wrap result in Markup.""" return Markup('{}').format(escape(value)) ``` #### Adding Background Jobs If you enabled background jobs, create tasks in `src/app/tasks/`: ```python # src/app/tasks/emails.py from vibetuner.tasks.worker import get_worker from vibetuner.models import UserModel from vibetuner.services.email import EmailService worker = get_worker() @worker.task() async def send_welcome_email(user_id: str): user = await UserModel.get(user_id) if user: email_service = EmailService() await email_service.send_email( to_address=user.email, subject="Welcome!", html_body="

Welcome!

", text_body="Welcome!", ) return {"status": "sent"} ``` Register tasks in `src/app/tasks/__init__.py`: ```python # src/app/tasks/__init__.py __all__ = ["emails"] from . import emails # noqa: F401 ``` Queue jobs from your routes: ```python from app.tasks.emails import send_welcome_email @router.post("/signup") async def signup(email: str): # Create user user = await create_user(email) # Queue background task task = await send_welcome_email.enqueue(str(user.id)) return {"message": "Welcome email queued", "task_id": task.id} ``` #### Custom Lifespan For custom startup/shutdown logic, create a lifespan and pass to `tune.py`: ```python # src/app/frontend/lifespan.py from contextlib import asynccontextmanager from fastapi import FastAPI from vibetuner.frontend.lifespan import base_lifespan @asynccontextmanager async def lifespan(app: FastAPI): async with base_lifespan(app): # Custom startup logic print("App starting with custom logic") yield # Custom shutdown logic print("App shutting down with custom logic") ``` ```python # src/app/tune.py from vibetuner import VibetunerApp from app.frontend.lifespan import lifespan app = VibetunerApp( frontend_lifespan=lifespan, ) ``` **Worker lifespan** (different signature — takes no arguments, yields context): ```python # src/app/tasks/lifespan.py from contextlib import asynccontextmanager from vibetuner.tasks.lifespan import base_lifespan @asynccontextmanager async def lifespan(): async with base_lifespan() as worker_context: # Custom worker startup logic print("Worker starting with custom logic") yield worker_context # Custom worker shutdown logic print("Worker shutting down") ``` ```python # src/app/tune.py from vibetuner import VibetunerApp from app.tasks.lifespan import lifespan as worker_lifespan app = VibetunerApp( worker_lifespan=worker_lifespan, ) ``` > **Note:** The frontend lifespan receives the `FastAPI` app and yields > nothing. The worker lifespan takes no arguments and yields a `Context` > object. #### Working with HTMX Vibetuner uses HTMX for interactive features without JavaScript: ```html
``` Server endpoint: ```python @router.get("/blog") async def list_posts(page: int = 1): posts = await Post.find().skip((page - 1) * 10).limit(10).to_list() return templates.TemplateResponse("blog/posts.html.jinja", { "posts": posts }) ``` #### HTMX Request Detection Every request has `request.state.htmx` available (via `starlette-htmx` middleware). Use it to serve different responses for HTMX vs regular requests: ```python from fastapi import Request from starlette.responses import HTMLResponse from vibetuner import render_template, render_template_string @router.get("/items") async def list_items(request: Request): items = await Item.find_all().to_list() ctx = {"items": items} if request.state.htmx: html = render_template_string("items/_list.html.jinja", request, ctx) return HTMLResponse(html) return render_template("items/list.html.jinja", request, ctx) ``` Properties: `bool(request.state.htmx)`, `.boosted`, `.target`, `.trigger`, `.trigger_name`, `.current_url`, `.prompt`. HTMX-only routes — use the `require_htmx` dependency from `vibetuner.frontend.deps` to reject non-HTMX requests with a 400. ### Architecture #### High-Level Overview Vibetuner generates full-stack web applications with clear separation between framework code and application code. #### Three-Package Architecture **1. Scaffolding Template** **Location**: Root repository (`copier.yml`, `vibetuner-template/`) The Copier-based template that generates new projects: - Interactive project setup - Configurable features (OAuth, background jobs, etc.) - Generates complete project structure - Updates existing projects **Command**: `uvx vibetuner scaffold new my-app` **2. Python Package (`vibetuner`)** **Location**: `vibetuner-py/` Published to PyPI, provides: - Core framework code - Authentication system - Database integration (MongoDB and SQL) - Email and storage services - CLI commands - Blessed dependency stack **Install**: `uv add vibetuner` **3. JavaScript Package (`@alltuner/vibetuner`)** **Location**: `vibetuner-js/` Published to npm, provides: - Frontend build dependencies (Tailwind, esbuild, etc.) - Version-locked with Python package - No runtime dependencies **Install**: `bun add @alltuner/vibetuner` #### Request Flow **1. HTTP Request** ```text Client → Nginx/Caddy → FastAPI (Granian) → Route Handler ``` **2. Route Handler** ```python # src/app/frontend/routes/blog.py @router.get("/blog/{post_id}") async def view_post(post_id: str): post = await Post.get(post_id) return templates.TemplateResponse("blog/post.html.jinja", { "post": post }) ``` **3. Database Query** ```text # MongoDB Route → Beanie ODM → Motor (async) → MongoDB # SQL Route → SQLModel → SQLAlchemy (async) → PostgreSQL/MySQL/SQLite ``` **4. Template Rendering** ```text Jinja2 Template → HTML with HTMX → Client ``` **5. HTMX Interaction** ```text User Action → HTMX Request → FastAPI → Partial HTML → Update DOM ``` ### Tech Stack #### Backend Stack **FastAPI** **Why:** Modern, fast, async-first Python web framework. - Automatic API documentation (OpenAPI/Swagger) - Async/await support throughout - Pydantic integration for validation - Type hints and IDE support - High performance (comparable to Node.js/Go) #### Database Options Vibetuner supports multiple database backends. All are optional - choose what fits your project. **MongoDB + Beanie ODM** **Why:** Flexible document database for rapid prototyping. - Schema flexibility during rapid development - Pydantic models are database models - Type-safe async operations - Automatic validation - Horizontal scaling with sharding **SQLModel + SQLAlchemy** **Why:** SQL databases when you need relational data. - PostgreSQL, MySQL, MariaDB, SQLite support - Pydantic + SQLAlchemy combined - Type-safe async operations - Full SQL power when needed - CLI command: `vibetuner db create-schema` **Granian** **Why:** High-performance ASGI server written in Rust. - Faster than Uvicorn/Gunicorn - Lower memory footprint - Built-in process management - Hot reload in development #### Frontend Stack **HTMX** **Why:** Interactivity without complex JavaScript. - Server-rendered HTML with dynamic updates - Progressive enhancement - Minimal client-side complexity - Works with any backend - Small footprint (~14kb) **Tailwind CSS** **Why:** Utility-first CSS framework. - Rapid UI development - No naming conventions needed - Automatic purging of unused CSS - Responsive design made simple - Customizable design system **DaisyUI** **Why:** Beautiful Tailwind CSS components. - Pre-built components - Theming support - No JavaScript required - Semantic class names - Accessibility built-in **Jinja2** **Why:** Powerful template engine for Python. - Template inheritance - Macros for reusable components - Filters and functions - i18n integration - Sandboxed execution #### Development Tools **uv** **Why:** Extremely fast Python package manager. - 10-100x faster than pip - Reliable dependency resolution - Compatible with pip/requirements.txt - Built in Rust **bun** **Why:** Fast all-in-one JavaScript runtime and toolkit. - 2-10x faster than npm/pnpm - Built-in bundler, transpiler, test runner - Drop-in Node.js replacement - Native TypeScript support **just** **Why:** Command runner (better Make). - Simple syntax - Cross-platform - Environment variable support - Recipe dependencies ### Authentication #### Overview Vibetuner includes: - **OAuth Authentication**: Google, GitHub, and more via Authlib - **Magic Link Authentication**: Passwordless email-based login - **Session Management**: Secure cookie-based sessions - **User Model**: Pre-configured with OAuth account linking #### OAuth Setup **Google OAuth** 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select existing 3. Enable Google+ API 4. Create OAuth 2.0 credentials 5. Add authorized redirect URI: `http://localhost:8000/auth/google/callback` Add to `.env`: ```bash GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret ``` **GitHub OAuth** 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 2. Create a new OAuth App 3. Set callback URL: `http://localhost:8000/auth/github/callback` Add to `.env`: ```bash GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret ``` #### Magic Link Authentication Magic links provide passwordless authentication via email. **Configuration** Magic links are enabled by default. Configure email settings: ```bash # .env SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=your-email@gmail.com SMTP_PASSWORD=your-app-password FROM_EMAIL=noreply@example.com ``` **How It Works** 1. User enters email address 2. System sends email with unique token 3. User clicks link in email 4. System validates token and logs user in 5. Session created #### Protecting Routes **Require Authentication** Use the `@require_auth` decorator: ```python from vibetuner.frontend.auth import require_auth @router.get("/dashboard") @require_auth async def dashboard(request: Request): user = request.state.user return templates.TemplateResponse("dashboard.html.jinja", { "user": user }) ``` **Optional Authentication** Access user if authenticated, but don't require it: ```python @router.get("/") async def home(request: Request): user = getattr(request.state, "user", None) return templates.TemplateResponse("home.html.jinja", { "user": user }) ``` ### CRUD Factory Vibetuner includes a CRUD route factory that generates list, create, read, update, and delete endpoints for Beanie Document models with one function call. ```python from vibetuner.crud import create_crud_routes, Operation from app.models import Post # Generate all CRUD routes post_routes = create_crud_routes( Post, prefix="/api/posts", tags=["posts"], sortable_fields=["created_at", "title"], filterable_fields=["status", "author_id"], searchable_fields=["title", "content"], page_size=25, max_page_size=100, ) ``` Register the router in `tune.py`: ```python app = VibetunerApp(routes=[post_routes]) ``` **Generated endpoints:** | Method | Path | Description | |--------|------|-------------| | GET | `/api/posts` | List with pagination, filtering, sorting, search | | POST | `/api/posts` | Create a new document | | GET | `/api/posts/{item_id}` | Read a single document | | PATCH | `/api/posts/{item_id}` | Update a document (partial) | | DELETE | `/api/posts/{item_id}` | Delete a document | **Query parameters for list:** - `offset` / `limit` — Pagination (default page size: 25) - `sort` — Comma-separated fields, prefix with `-` for descending (e.g., `?sort=-created_at`) - `search` — Text search across searchable fields (case-insensitive regex) - `fields` — Comma-separated field names to include in response - Any filterable field as a query param (e.g., `?status=published`) **Limiting operations:** ```python # Only generate list and read (no create/update/delete) post_routes = create_crud_routes( Post, operations={Operation.LIST, Operation.READ}, ) ``` **Custom schemas and hooks:** ```python from pydantic import BaseModel class PostCreate(BaseModel): title: str content: str class PostResponse(BaseModel): id: str title: str async def before_create(data, request): data.author_id = request.state.user.id return data post_routes = create_crud_routes( Post, create_schema=PostCreate, response_schema=PostResponse, pre_create=before_create, post_create=lambda doc, req: print(f"Created {doc.id}"), pre_update=lambda doc, data, req: data, post_update=lambda doc, req: None, pre_delete=lambda doc, req: None, post_delete=lambda doc, req: None, dependencies=[require_auth], ) ``` **Hook signatures reference:** All hooks are async callables with these signatures: | Hook | Signature | Returns | |------|-----------|---------| | `pre_create` | `async (data, request)` | Modified data or `None` | | `post_create` | `async (doc, request)` | `None` | | `pre_update` | `async (doc, data, request)` | Modified data or `None` | | `post_update` | `async (doc, request)` | `None` | | `pre_delete` | `async (doc, request)` | `None` | | `post_delete` | `async (doc, request)` | `None` | `pre_create` and `pre_update` can return the (modified) data object to override input. If they return `None`, the original data is used. ### Server-Sent Events (SSE) Vibetuner provides SSE helpers for real-time streaming with HTMX. Import from `vibetuner.sse` (not `vibetuner.frontend.sse`). ```python from vibetuner.sse import sse_endpoint, broadcast ``` **Channel-based endpoint (auto-subscribe):** ```python from fastapi import APIRouter, Request from vibetuner.sse import sse_endpoint router = APIRouter() @sse_endpoint("/events/notifications", channel="notifications", router=router) async def notifications_stream(request: Request): pass # channel kwarg handles everything ``` **Dynamic channel based on path params:** ```python @sse_endpoint("/events/room/{room_id}", router=router) async def room_stream(request: Request, room_id: str): return f"room:{room_id}" # return channel name ``` **Generator-based (full control):** ```python import asyncio @sse_endpoint("/events/custom", router=router) async def custom_stream(request: Request): while True: yield {"event": "tick", "data": "ping"} await asyncio.sleep(5) ``` **Broadcasting events:** ```python from vibetuner.sse import broadcast # Raw data await broadcast("notifications", "update", data="
New item!
") # With template rendering await broadcast( "feed", "new-post", template="partials/post.html.jinja", request=request, ctx={"post": post}, ) ``` SSE uses in-process channels by default. When Redis is configured, events are automatically bridged via Redis pub/sub for multi-worker support. **HTMX client example:** ```html
``` ### Template Context Providers Register static globals or dynamic context providers available in every template. **Static globals:** ```python from vibetuner.rendering import register_globals register_globals({ "site_title": "My App", "og_image": "/static/og.png", }) ``` **Dynamic providers (called on every render):** ```python from vibetuner.rendering import register_context_provider @register_context_provider def site_context() -> dict: return {"site_title": settings.site_title, "year": 2025} # Also works with parentheses @register_context_provider() def analytics_context() -> dict: return {"analytics_id": "UA-XXX"} ``` Values from providers and globals are merged into every `render_template()` call. User-provided context in the render call takes precedence. ### Service Dependency Injection Vibetuner provides FastAPI `Depends()` wrappers for built-in services: ```python from fastapi import Depends from vibetuner.services import ( get_email_service, get_blob_service, get_runtime_config, ) @router.post("/send") async def send(email=Depends(get_email_service)): await email.send_email(to_address="user@example.com", ...) @router.post("/upload") async def upload(blobs=Depends(get_blob_service)): await blobs.upload(file_data, filename) @router.get("/settings") async def settings_page(config=Depends(get_runtime_config)): value = await config.get("features.dark_mode") ``` ### Health Check Endpoints Built-in health check routes are mounted at `/health`: | Endpoint | Purpose | |----------|---------| | `GET /health/ping` | Liveness probe — always fast, no external calls | | `GET /health` | Basic health with uptime. Add `?detailed=true` for service checks | | `GET /health/ready` | Readiness probe — checks all configured services | | `GET /health/id` | Instance identification (app name, port, PID, startup time) | The detailed health check and readiness probe test MongoDB, Redis, S3/R2, and email connectivity with latency measurements. Returns `"degraded"` or `"not_ready"` if any required service is down. ### Robust Tasks (Retries & Dead Letters) For tasks that need automatic retry with exponential backoff: ```python from vibetuner.tasks.robust import robust_task @robust_task(max_retries=5, backoff_base=2.0, backoff_max=600) async def send_report(account_id: str): # If this raises, it retries with exponential backoff ... ``` **Parameters:** - `max_retries` — Maximum attempts before giving up (default: 3) - `backoff_base` — Base for exponential backoff: delay = base^tries (default: 2.0) - `backoff_max` — Maximum backoff delay in seconds (default: 300) - `timeout` — Task timeout (forwarded to Streaq) - `on_failure` — Callback on permanent failure: `(task_name, task_id, exception)` After all retries are exhausted, the task is saved to the `dead_letters` MongoDB collection as a `DeadLetterModel` document with task name, error details, and try count. ### Streaq Task Queue UI When background workers are configured, Vibetuner automatically mounts the Streaq task queue UI at `/debug/tasks`. This provides a web interface for monitoring queued, running, and completed tasks. Access is restricted by the same debug access check used for other debug routes. ### `vibetuner doctor` A diagnostic CLI command that validates your project setup: ```bash vibetuner doctor ``` Checks performed: - **Project structure** — root directory, `.copier-answers.yml`, `.env`, `src/` layout - **App configuration** — `tune.py` loads without errors - **Environment** — environment mode, SESSION_KEY - **Service connectivity** — MongoDB, Redis, R2/S3 reachability - **Models** — registered Beanie models - **Templates** — template directory exists, basic syntax checks - **Dependencies** — vibetuner, FastAPI, Beanie, Granian versions - **Ports** — 8000 (frontend) and 11111 (worker UI) availability Output uses colored status icons and a summary table. Exits with code 1 if any errors are found. ### Config Decorators and ConfigGroup In addition to `register_config_value()`, you can use decorators and classes: **`@config_value` decorator:** ```python from vibetuner.runtime_config import config_value @config_value("features.dark_mode", value_type="bool", category="features") def dark_mode() -> bool: return False # default value # Later, in an async context: enabled = await dark_mode() ``` **`ConfigGroup` class:** ```python from vibetuner.runtime_config import ConfigGroup, ConfigField class FeatureFlags(ConfigGroup, category="features"): dark_mode = ConfigField( default=False, value_type="bool", description="Enable dark mode" ) max_items = ConfigField( default=50, value_type="int", description="Max items per page" ) # Later: enabled = await FeatureFlags.dark_mode max_items = await FeatureFlags.max_items ``` All registered config values are viewable and editable at `/debug/config`. ### Testing Utilities Vibetuner provides pytest fixtures for testing your application. Install the plugin by adding `vibetuner` to your test dependencies — fixtures are auto-discovered. **Available fixtures:** | Fixture | Description | |---------|-------------| | `vibetuner_client` | Async HTTP test client with full middleware | | `vibetuner_app` | The FastAPI app instance (override for custom apps) | | `vibetuner_db` | Temporary MongoDB database with Beanie initialized | | `mock_auth` | Mock authentication without real sessions | | `mock_tasks` | Mock background tasks without Redis | | `override_config` | Override RuntimeConfig values with auto-cleanup | **Example test:** ```python import pytest from unittest.mock import patch @pytest.mark.asyncio async def test_dashboard(vibetuner_client, mock_auth): mock_auth.login(name="Alice", email="alice@example.com") resp = await vibetuner_client.get("/dashboard") assert resp.status_code == 200 @pytest.mark.asyncio async def test_signup_sends_email(vibetuner_client, mock_tasks): with patch( "app.tasks.emails.send_welcome_email", mock_tasks.send_welcome_email, ): resp = await vibetuner_client.post("/signup", data={...}) assert mock_tasks.send_welcome_email.enqueue.called @pytest.mark.asyncio async def test_feature_flag(override_config): await override_config("features.dark_mode", True) from vibetuner.runtime_config import RuntimeConfig value = await RuntimeConfig.get("features.dark_mode") assert value is True ``` ### Default Language Configuration Set the default language and supported languages in your project settings: ```bash # .env or project config DEFAULT_LANGUAGE=en SUPPORTED_LANGUAGES=["es","fr","ca"] ``` The `default_language` setting (defaults to `"en"`) controls the locale used when no language is detected from the request. It affects template rendering, i18n middleware, and hreflang tag generation. ### Centralized Error Messages When a service is not configured (MongoDB, Redis, S3, email), vibetuner displays actionable error messages with: - The exact `.env` variables needed - Example values - Docker commands for local development - Links to documentation - Hints for disabling the service if not needed These errors are displayed as rich console panels via `vibetuner.services.errors`. ### Background Tasks Run long-running or scheduled work outside the request cycle using Vibetuner's background task system, powered by Streaq and Redis. **Note:** In all examples, `app` refers to your project's Python package (the directory under `src/`). The actual name depends on your project slug. #### Prerequisites Background tasks require Redis. Set `REDIS_URL` in your `.env`: ```bash REDIS_URL=redis://localhost:6379 ``` When using Docker development (`just dev`), Redis is started automatically. #### Quick Start 1. Create a task in `src/app/tasks/`: ```python # src/app/tasks/emails.py from vibetuner.tasks.worker import get_worker worker = get_worker() @worker.task() async def send_welcome_email(user_id: str) -> dict[str, str]: from vibetuner.models import UserModel from vibetuner.services.email import EmailService user = await UserModel.get(user_id) if user: email_service = EmailService() await email_service.send_email( to_address=user.email, subject="Welcome!", html_body="

Welcome!

", text_body="Welcome!", ) return {"status": "sent", "email": user.email} return {"status": "skipped"} ``` 2. Register in `tune.py`: ```python from vibetuner import VibetunerApp from app.tasks.emails import send_welcome_email app = VibetunerApp(tasks=[send_welcome_email]) ``` 3. Enqueue from a route: ```python task = await send_welcome_email.enqueue(str(user.id)) ``` 4. Start the worker: ```bash just worker-dev # Or: uv run vibetuner run dev worker ``` #### Worker Dependency Injection Use `WorkerDepends()` from Streaq to inject the worker context: ```python from streaq import WorkerDepends @worker.task() async def fetch_external_data(url: str, ctx=WorkerDepends()) -> dict: response = await ctx.http_client.get(url) return {"status": response.status_code, "data": response.text[:100]} ``` #### The `@robust_task()` Decorator For tasks needing automatic retries and dead letter tracking: ```python from vibetuner.tasks.robust import robust_task @robust_task(max_retries=5, backoff_max=600) async def send_webhook(payload: dict) -> dict: import httpx async with httpx.AsyncClient() as client: resp = await client.post("https://example.com/hook", json=payload) resp.raise_for_status() return {"status": "delivered"} ``` Parameters: `max_retries` (default 3), `backoff_base` (default 2.0), `backoff_max` (default 300.0), `timeout`, `on_failure` callback. Failed tasks are stored in the `dead_letters` MongoDB collection. #### Scheduled Tasks (Cron) ```python @worker.cron("0 9 * * *") # Every day at 9:00 AM UTC async def daily_digest(): ... @worker.cron("*/15 * * * *") # Every 15 minutes async def check_expired_sessions(): ... ``` Cron tasks only execute inside the worker process. #### SSE Integration Broadcast real-time updates from background tasks to connected clients: ```python from vibetuner.sse import broadcast @worker.task() async def process_upload(file_id: str, user_id: str) -> dict: await broadcast(f"upload:{user_id}", "progress", data="Processing...") # ... do work ... await broadcast(f"upload:{user_id}", "complete", data="Done!") return {"status": "complete"} ``` Broadcasting from tasks requires Redis for pub/sub across processes. #### Custom Worker Lifespan ```python from contextlib import asynccontextmanager from vibetuner.context import Context from vibetuner.tasks.lifespan import base_lifespan @asynccontextmanager async def worker_lifespan(): async with base_lifespan() as context: print("Worker starting with custom setup") yield context print("Worker shutting down") ``` #### Running Workers ```bash # Development just worker-dev just local-all-with-worker # Production docker compose -f compose.prod.yml up vibetuner run prod worker --workers 4 ``` Configuration: `REDIS_URL` (required), `WORKER_CONCURRENCY` (default 16), `--port` flag (default 11111 for monitoring UI). #### Testing Tasks Use the `mock_tasks` fixture: ```python async def test_signup_queues_email(vibetuner_client, mock_tasks): with patch("app.tasks.emails.send_welcome_email", mock_tasks.send_welcome_email): resp = await vibetuner_client.post("/signup", data={"email": "a@b.com"}) assert mock_tasks.send_welcome_email.enqueue.called ``` ### Runtime Configuration A layered configuration system for managing application settings that can be changed at runtime and optionally persisted to MongoDB. #### Overview Runtime configuration provides settings that: - Can be changed without redeploying the application - Persist across server restarts (when MongoDB is available) - Support in-memory overrides for debugging and testing - Integrate with a debug UI at `/debug/config` This is separate from `CoreConfiguration` (`.env` settings) which handles framework-level settings loaded at startup. #### Register Configuration Values Register config values at module load time, typically in `src/app/config.py`: ```python from vibetuner.runtime_config import register_config_value register_config_value( key="features.dark_mode", default=False, value_type="bool", category="features", description="Enable dark mode for users", ) register_config_value( key="limits.max_uploads", default=10, value_type="int", category="limits", description="Maximum uploads per user per day", ) ``` Secret values (`is_secret=True`) are masked in the debug UI and cannot be edited. #### Access Configuration Values ```python from vibetuner.runtime_config import get_config async def some_handler(): dark_mode = await get_config("features.dark_mode") max_items = await get_config("unknown.key", default=50) ``` #### Layered Resolution Values are resolved with this priority (highest to lowest): 1. **Runtime overrides** — In-memory overrides set programmatically or via debug UI 2. **MongoDB values** — Persisted values that survive restarts 3. **Registered defaults** — Default values defined in code #### Value Types Supported types: `bool`, `int`, `float`, `str`, `json`. Values are automatically validated and converted when set. #### `@config_value` Decorator ```python from vibetuner.runtime_config import config_value @config_value("features.dark_mode", value_type="bool", category="features") def dark_mode() -> bool: """Enable dark mode for users.""" return False enabled = await dark_mode() ``` #### `ConfigGroup` Class Group related config values into a typed class: ```python from vibetuner.runtime_config import ConfigGroup, ConfigField class FeatureFlags(ConfigGroup, category="features"): dark_mode = ConfigField(default=False, value_type="bool", description="Enable dark mode") max_items = ConfigField(default=50, value_type="int", description="Max items per page") enabled = await FeatureFlags.dark_mode limit = await FeatureFlags.max_items ``` Each field is registered under `"{category}.{field_name}"`. #### When to Use Each API | API | Best for | |-----|----------| | `register_config_value()` | Imperative registration at module level | | `@config_value()` | Single standalone config values with defaults | | `ConfigGroup` | Groups of related settings (feature flags, limits) | #### MongoDB Persistence and Caching When MongoDB is configured, values can be persisted and survive restarts. Config values from MongoDB are cached for 60 seconds. Use `RuntimeConfig.refresh_cache()` or the debug UI refresh button to force refresh. ### HTMX v2 to v4 Migration Breaking changes when upgrading from htmx v2 to v4 in Vibetuner projects. #### SSE: Native Support Replaces Extension htmx v4 includes SSE in core. Remove `hx-ext="sse"`: ```html
``` #### Extension Auto-Registration Extensions auto-register when imported — no `hx-ext` attribute needed. #### `hx-vars` Replaced by `hx-vals` with `js:` Prefix ```html hx-vals='js:{"token": getToken()}' ``` #### `hx-disable` Renamed to `hx-ignore` ```html
``` #### Attribute Inheritance Requires `:inherited` In v4, inheritance must be explicitly opted into: ```html
``` #### JavaScript Import Changes ```javascript // Before (v2): import "htmx.org"; // After (v4): import htmx from "htmx.org"; window.htmx = htmx; // Preload extension: // Before: import "htmx-ext-preload"; // After: import "htmx.org/dist/ext/hx-preload.js"; ``` #### Migration Checklist - Remove all `hx-ext="sse"` attributes from SSE elements - Remove all other `hx-ext="..."` attributes (extensions auto-register) - Replace `hx-vars` with `hx-vals` using `js:` prefix - Replace `hx-disable` with `hx-ignore` - Add `:inherited` modifier to attributes that rely on inheritance - Update JS imports to use default import and `window.htmx = htmx` - Update preload extension import path - Remove `htmx-ext-sse` and `htmx-ext-preload` from `package.json` ### Import Ordering in tune.py When configuring `tune.py`, import order matters. Vibetuner framework imports (like `from vibetuner import VibetunerApp`) must come before your application imports (like `from app.models import Post`). This ensures the framework is initialized before your application code tries to use it. ```python # src/app/tune.py — correct order from vibetuner import VibetunerApp # framework first from app.models import Post, Comment # then your app from app.frontend.routes import app_router app = VibetunerApp( models=[Post, Comment], # Beanie (MongoDB) models sql_models=[SqlPost], # SQLModel (SQL) models routes=[app_router], ) ``` ## Reference ### CLI Reference #### `vibetuner scaffold` **`new`** ```bash vibetuner scaffold new DESTINATION [options] ``` Creates a project from the local Vibetuner Copier template. **Options** - `--template`, `-t` – Use a different template source (local path, git URL, `github:user/repo`, etc.). - `--defaults`, `-d` – Accept default answers for every prompt (non-interactive). - `--data key=value` – Override individual template variables. Repeat for multiple overrides. - `--branch`, `-b` – Use specific branch/tag from the vibetuner template repository. - `DESTINATION` must not already exist. **Examples** ```bash # Interactive run vibetuner scaffold new my-project # Non-interactive defaults vibetuner scaffold new my-project --defaults # Override selected values in non-interactive mode vibetuner scaffold new my-project \ --defaults \ --data project_name="My Project" \ --data python_version="3.12" # Test from a specific branch vibetuner scaffold new my-project --branch fix/scaffold-command ``` **`update`** ```bash vibetuner scaffold update [PATH] [options] ``` Brings an existing project up to date with the current template. - `PATH` defaults to the current directory. - `--skip-answered / --no-skip-answered` controls whether previously answered prompts are re-asked (defaults to skipping). - `--branch`, `-b` – Use specific branch/tag from the vibetuner template repository. - Exits with an error if `.copier-answers.yml` is missing. **`adopt`** ```bash vibetuner scaffold adopt [PATH] [options] ``` Adopts vibetuner scaffolding for an existing project that already has vibetuner as a dependency. - `PATH` defaults to the current directory. - Requires `pyproject.toml` with vibetuner in dependencies. - Fails if `.copier-answers.yml` already exists (use `update` instead). **Options** - `--defaults`, `-d` – Accept default answers for every prompt (non-interactive). - `--data key=value` – Override individual template variables. - `--branch`, `-b` – Use specific branch/tag from the vibetuner template repository. #### `vibetuner run` Starts framework services without Docker. **`dev`** ```bash vibetuner run dev [frontend|worker] [--port PORT] [--host HOST] [--workers COUNT] [--auto-port] ``` - `--auto-port` – Use deterministic port based on project path (8001-8999). Mutually exclusive with `--port`. - Sets `DEBUG=1` and enables hot reload. - `service` defaults to `frontend`. - Frontend watches `src/app/` and `templates/` for changes. - Worker runs the Streaq worker with reload enabled (ignores `--workers` > 1). **`prod`** ```bash vibetuner run prod [frontend|worker] [--port PORT] [--host HOST] [--workers COUNT] ``` - Sets `ENVIRONMENT=production`. - Disables hot reload and honors `--workers` for both frontend and worker services. - Useful for containerless deployments or reproducing production settings locally. #### `vibetuner db` Database management commands for SQL databases (SQLModel/SQLAlchemy). **`create-schema`** ```bash vibetuner db create-schema ``` Creates all database tables defined in SQLModel metadata. This command: 1. Imports models from `app.models` to ensure they're registered 2. Creates tables in the database specified by `DATABASE_URL` 3. Skips if tables already exist (safe to run multiple times) **Prerequisites:** - `DATABASE_URL` environment variable must be set - Models must be defined using SQLModel with `table=True` **Note:** This command is only for SQL databases. MongoDB collections are created automatically when documents are inserted. #### `vibetuner doctor` Run diagnostic checks on your project: ```bash vibetuner doctor ``` Validates project structure, configuration, service connectivity, models, templates, dependencies, and port availability. Exits with code 1 if errors are found. #### `vibetuner version` Show version information. ```bash vibetuner version [--app] ``` **Options** - `--app`, `-a` – Show app settings version even if not in a project directory. ### Scaffolding Reference #### Template Prompts - `company_name` – default `Acme Corp`. Displayed in generated metadata, email templates, and documentation. - `author_name` – default `Developer`. Used for attribution in project metadata. - `author_email` – default `dev@example.com`. Included in scaffolded docs and config. - `project_name` – default `_folder_name`. Human-friendly name used throughout the README and docs. - `project_slug` – default `_folder_name`. Must match `^[a-z][a-z0-9\-]*[a-z0-9]$` (used for Python package name, Docker image, compose project name). - `project_description` – default `A project that does something useful.` Populates package metadata. - `fqdn` – default empty. When set, enables production deployment extras (Caddy, Watchtower, etc.). - `python_version` – default `3.14`. Controls `.python-version`, Docker images, and `uv` settings. - `supported_languages` – default `[]`. JSON/YAML list of language codes (for example `["es", "fr"]`), adds translation skeletons. - `redis_url` – default empty. Used when background jobs are enabled. - `mongodb_url` – default empty. MongoDB connection string written to `.env.local`. - `database_url` – default empty. SQL database connection string (PostgreSQL, MySQL, MariaDB, SQLite) written to `.env.local`. - `enable_watchtower` – default `false`. Only prompted when `fqdn` is set; adds Watchtower service to production Docker Compose. #### Updating Existing Projects There are two supported update flows: 1. **`vibetuner scaffold update`** (works everywhere) - Reads `.copier-answers.yml` and reapplies the latest template files. - Respects previous answers by default; pass `--no-skip-answered` to revisit prompts. - Runs the same post-generation tasks after updating files. 2. **`just update-scaffolding`** (inside generated projects) - Shells out to `copier update -A --trust`, then re-syncs dependencies (`bun install`, `uv sync --all-extras`). - Useful when you already have the project checked out with scaffolded `justfile`. Both commands update tracked files. Always commit or stash local changes before running them, review the results, and resolve any merge prompts Copier surfaces. ### Deployment #### Docker Production Build Vibetuner includes a multi-stage Dockerfile optimized for production. **Test Production Build Locally** ```bash just test-build-prod ``` This builds and runs the production image locally. **Build and Push** ```bash just release ``` This builds the production image and pushes to your container registry. #### Environment Configuration **Production Environment Variables** Create a `.env` file for production: ```bash # Application APP_NAME=My Application SECRET_KEY=your-production-secret-key DEBUG=false ENVIRONMENT=production # Database (MongoDB) MONGODB_URL=mongodb://user:password@mongodb-host:27017/myapp?authSource=admin # Or SQL database (PostgreSQL, MySQL, MariaDB, SQLite) DATABASE_URL=postgresql+asyncpg://user:password@postgres-host:5432/myapp # Redis (if background jobs enabled) REDIS_URL=redis://redis-host:6379/0 # Email SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_USER=apikey SMTP_PASSWORD=your-sendgrid-api-key FROM_EMAIL=noreply@example.com # OAuth GOOGLE_CLIENT_ID=your-production-client-id GOOGLE_CLIENT_SECRET=your-production-secret GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-secret # Session SESSION_COOKIE_SECURE=true SESSION_COOKIE_SAMESITE=lax SESSION_MAX_AGE=2592000 # Storage (if using S3) AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key AWS_S3_BUCKET=your-bucket AWS_REGION=us-east-1 ``` #### Deployment Options **Docker Compose** Use `compose.prod.yml` for production deployment: ```bash docker compose -f compose.prod.yml up -d ``` This starts: - Database with persistence (MongoDB or PostgreSQL) - Redis (if enabled) - Your application **Cloud Platforms** *Railway* 1. Connect GitHub repository 2. Add database plugin (MongoDB or PostgreSQL) 3. Configure environment variables 4. Deploy automatically on push *Render* 1. Create new Web Service 2. Connect repository 3. Add environment variables 4. Render will auto-deploy ## Development ### Contributing Guidelines #### About This Project Vibetuner was born to meet the needs of [All Tuner Labs](https://alltuner.com) when spawning new projects. It embodies our core beliefs: - **Simplicity**: Minimal boilerplate, clear conventions - **Speed of Iteration**: Fast development cycles, hot reload, minimal friction - **Modern Technology Stack**: Latest stable versions, async-first architecture - **Good Integration with Coding Assistants**: Well-documented, predictable patterns #### About Contributions While we welcome feedback and are happy to see community interest, please note: - **We may not accept all pull requests** - The project serves specific internal needs and design goals - **Response times may vary** - This is one of many projects we maintain - **Breaking changes may occur** - We prioritize our internal requirements - **No guarantees on feature requests** - We'll consider them, but can't commit to implementing everything That said, we do appreciate good contributions that align with our core beliefs (simplicity, speed, modern stack, assistant-friendly) and will do our best to review them when time allows. #### PR Title Format (Important!) This project uses **Release Please** for automated changelog generation. Since we squash PRs, **PR title becomes the commit message** that determines version bumps and changelog entries. **Required Format** ```text [optional scope]: ``` **Supported Types and Version Impact** | Type | Description | Version Impact | |------|-------------|----------------| | `feat` | New features | **MINOR** | | `fix` | Bug fixes | **PATCH** | | `docs` | Documentation changes | **PATCH** | | `chore` | Maintenance, dependencies | **PATCH** | | `refactor` | Code refactoring | **PATCH** | | `style` | Formatting, linting | **PATCH** | | `test` | Test changes | **PATCH** | | `perf` | Performance improvements | **MINOR** | | `ci` | CI/CD changes | **PATCH** | | `build` | Build system changes | **PATCH** | **Breaking Changes** Add `!` to indicate breaking changes (triggers **MAJOR** version): - `feat!: remove deprecated API` - `fix!: change database schema` **Examples** *Good PR Titles* ```text feat: add OAuth authentication support fix: resolve Docker build failure docs: update installation guide chore: bump FastAPI dependency feat(auth): add Google OAuth provider feat!: remove deprecated authentication system ``` *Bad PR Titles (Release Please can't categorize)* ```text Add OAuth Fix Docker Update docs Authentication changes OAuth implementation ``` ## Packages ### Python Package **Location**: `vibetuner-py/` Published to PyPI, provides: - Core framework code - Authentication system - Database integration (MongoDB and SQL) - Email and storage services - CLI commands - Blessed dependency stack **Install**: `uv add vibetuner` **Dependencies**: - FastAPI with async support - MongoDB with Beanie ODM - SQLModel with SQLAlchemy - Granian ASGI server - Authlib for OAuth - HTMX integration - Background job support (Redis/Streaq) - Email services - CLI tools ### JavaScript Package **Location**: `vibetuner-js/` Published to npm, provides: - Frontend build dependencies (Tailwind, esbuild, etc.) - Version-locked with Python package - No runtime dependencies **Install**: `bun add @alltuner/vibetuner` **Dependencies**: - Tailwind CSS v4 - DaisyUI components - HTMX and extensions - Build tools ### Repository **Source Code**: [github.com/alltuner/vibetuner](https://github.com/alltuner/vibetuner) Contains: - Scaffolding template (`vibetuner-template/`) - Python package source (`vibetuner-py/`) - JavaScript package source (`vibetuner-js/`) - Documentation (`vibetuner-docs/`) - CI/CD workflows (`.github/workflows/`) **Issue Tracking**: Use GitHub Issues for bug reports and feature requests. ## Optional ### Development Setup #### Setting Up the Development Environment ```bash # Clone repository git clone https://github.com/alltuner/vibetuner cd vibetuner # Set up Python package cd vibetuner-py uv sync # Set up JavaScript package cd ../vibetuner-js bun install # Test scaffold command uv run --directory ../vibetuner-py vibetuner scaffold new /tmp/test-project ``` #### Code Style - **Python**: Follow existing style, use Ruff for formatting - **JavaScript**: Follow existing style - **Commits**: Clear, concise messages describing "why" #### Testing Before submitting: ```bash # Format Python code cd vibetuner-py ruff format . # Check for issues ruff check . # Test scaffold command uv run vibetuner scaffold new /tmp/test-project --defaults cd /tmp/test-project just dev # Should start without errors ``` ### Internal Documentation The `vibetuner-docs/docs/` directory contains the source files for all documentation. These are built using MkDocs and deployed to GitHub Pages. **Documentation Structure**: - `index.md` - Main landing page - `quick-start.md` - Getting started guide - `development-guide.md` - Daily development workflow - `architecture.md` - System design and structure - `tech-stack.md` - Technology choices and rationale - `authentication.md` - Authentication setup and configuration - `deployment.md` - Production deployment guide - `cli-reference.md` - Command-line reference - `scaffolding.md` - Template customization - `contributing.md` - Contribution guidelines - `development.md` - Development setup for contributors - `changelog.md` - Version history **Building Documentation**: ```bash just docs-serve # Local development with live reload just docs-build # Production build ``` The documentation is automatically built and deployed to `https://vibetuner.alltuner.com/` on releases.