Skip to content

Development Guide

Daily development workflow for Vibetuner projects.

Development Environment

Vibetuner supports two development modes:

Run everything in containers with hot reload:

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:

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

Justfile Commands Reference

All project management tasks use just (command runner). Run just without arguments to see all available commands.

Development

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

just install-deps            # Install from lockfiles
just update-repo-deps        # Update root scaffolding dependencies
just update-and-commit-repo-deps  # Update deps and commit changes
uv add package-name          # Add Python package
bun add package-name         # Add JavaScript package

Code Formatting

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

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 (i18n)

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)
just dump-untranslated DIR   # Export untranslated strings

CI/CD & Deployment

just build-dev               # Build development Docker image
just test-build-prod         # Test production build locally
just build-prod              # Build production image (requires clean tagged commit)
just release                 # Build and release production image
just deploy-latest HOST      # Deploy to remote host

Scaffolding Updates

just update-scaffolding      # Update project to latest vibetuner template

Common Tasks

Adding New Routes

Create a new file in src/app/frontend/routes/:

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

from app.frontend.routes import blog
app.include_router(blog.router)

Adding Database Models

Create models in src/app/models/. The approach depends on your database choice:

MongoDB (Beanie ODM)

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

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

from app.models.post import Post
__all__ = ["Post"]

Creating Templates

Add templates in templates/:

<!-- templates/blog/list.html.jinja -->
{% extends "base/skeleton.html.jinja" %}
{% block content %}
    <div class="container mx-auto">
        <h1 class="text-3xl font-bold">Blog Posts</h1>
        <div class="grid gap-4">
            {% for post in posts %}
                <article class="card">
                    <h2>{{ post.title }}</h2>
                    <div>{{ post.content }}</div>
                </article>
            {% endfor %}
        </div>
    </div>
{% endblock %}

Adding Custom Template Filters

Create custom Jinja2 filters in src/app/frontend/templates.py:

# src/app/frontend/templates.py
from vibetuner.frontend.templates import register_filter

@register_filter()
def uppercase(value):
    """Convert value to uppercase"""
    return str(value).upper()

@register_filter("money")
def format_money(value):
    """Format value as USD currency"""
    try:
        return f"${float(value):,.2f}"
    except (ValueError, TypeError):
        return str(value)

Use in templates:

<h1>{{ user.name | uppercase }}</h1>
<p>Price: {{ product.price | money }}</p>

The @register_filter() decorator automatically registers filters with the Jinja environment. If no name is provided, the function name becomes the filter name.

Adding Background Jobs

If you enabled background jobs, create tasks in src/app/tasks/:

# src/app/tasks/emails.py
from vibetuner.tasks.worker import worker
from vibetuner.models import UserModel
from vibetuner.services.email import send_email

@worker.task()
async def send_welcome_email(user_id: str):
    user = await UserModel.get(user_id)
    if user:
        await send_email(
            to_email=user.email,
            subject="Welcome!",
            html_content="<h1>Welcome!</h1>"
        )
    return {"status": "sent"}

Register tasks in src/app/tasks/__init__.py:

# src/app/tasks/__init__.py
__all__ = ["emails"]
from . import emails  # noqa: F401

Queue jobs from your routes:

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}

Styling with Tailwind

Vibetuner uses Tailwind CSS + DaisyUI. Edit assets/config.css for custom styles:

/* assets/config.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
.btn-custom {
@apply btn btn-primary rounded-full;
}

The build process automatically compiles to assets/statics/css/bundle.css.

Working with HTMX

Vibetuner uses HTMX for interactive features without JavaScript:

<!-- Load more posts -->
<button hx-get="/blog?page=2"
        hx-target="#posts"
        hx-swap="beforeend"
        class="btn btn-primary">Load More</button>
<div id="posts">
    <!-- Posts will be appended here -->
</div>

Server endpoint:

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

Internationalization

Extracting Translations

After adding translatable strings:

just extract-translations

This scans your code and templates for {% trans %} blocks and gettext() calls.

Adding New Languages

just new-locale es  # Spanish
just new-locale fr  # French

Updating Translations

Edit .po files in translations/:

# translations/es/LC_MESSAGES/messages.po
msgid "Welcome"
msgstr "Bienvenido"

Compile translations:

just compile-locales

Using in Templates

{% trans %}Welcome to {{ app_name }}{% endtrans %}

Using in Python

from starlette_babel import gettext as _
message = _("Welcome to {app}", app=app_name)

Debugging

View Logs

# Docker mode
docker compose logs -f web
# Local mode
# Logs print to stdout

Access Database

# MongoDB
docker compose exec mongodb mongosh

# PostgreSQL
docker compose exec postgres psql -U postgres

Interactive Shell

# Python shell with app context
just shell

Testing

Run Tests

pytest

Test Coverage

pytest --cov=src/app

Integration Tests

Integration tests should use a real database (not mocks):

import pytest
from app.models import Post

@pytest.mark.asyncio
async def test_create_post():
    post = Post(title="Test", content="Content")
    await post.insert()  # MongoDB with Beanie
    found = await Post.get(post.id)
    assert found.title == "Test"

Code Quality

Format Code

just format

Runs:

  • ruff format for Python

Check Code

just lint

Runs:

  • ruff check for Python
  • Type checking
  • Template validation

Environment Configuration

Development Settings

Copy .env.local to .env:

cp .env.local .env

Edit as needed:

# .env
# MongoDB
MONGODB_URL=mongodb://localhost:27017/myapp
# Or SQL database (PostgreSQL, MySQL, MariaDB, SQLite)
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/myapp
# DATABASE_URL=sqlite+aiosqlite:///./data.db

SECRET_KEY=your-secret-key-here
DEBUG=true
# OAuth (optional)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...

Production Settings

Use environment variables or .env file in production:

# MongoDB
MONGODB_URL=mongodb://prod-server:27017/myapp
# Or SQL database
DATABASE_URL=postgresql+asyncpg://user:pass@prod-server/myapp

SECRET_KEY=very-secret-key
DEBUG=false

Keeping the Scaffold Up to Date

When new versions of the template ship, update your project using either:

  • vibetuner scaffold update – works from anywhere; replays Copier with your saved answers.
  • just update-scaffolding – runs inside the generated project and wraps copier update plus dependency sync.

Both commands modify tracked files, so commit or stash your work beforehand and review the changes afterward. See the Scaffolding Reference for a deeper walkthrough.

Dependency Management

Add Python Package

uv add package-name

Add JavaScript Package

bun add package-name

Sync All Dependencies

just sync

Syncs both Python and JavaScript dependencies.

Next Steps