Rails for Rust. An opinionated full-stack application template: Rust gRPC backend + Nuxt 4 frontend, connected via Protocol Buffers for end-to-end type safety.
Write your backend in Rust, your frontend in Vue/Nuxt, and get automatically generated TypeScript types from your .proto files. Bufstack makes strong choices so you don't have to -- just build your app.
Bufstack is deliberately opinionated. Instead of giving you a blank canvas and infinite choices, we pick the best tools and wire them together so you can focus on building features:
- Protocol Buffers for schema definition -- your
.protofiles are the single source of truth for types across the entire stack - gRPC (via Tonic) for backend services -- strongly typed, fast, streaming-capable RPCs instead of hand-rolled REST endpoints
- SQLx with SQLite for data access -- compile-time checked SQL queries with zero overhead, no ORM magic
- Clerk for authentication -- drop-in auth that handles JWTs, sessions, and user management so you never roll your own
- Tailwind CSS v4 + shadcn-vue for styling -- utility-first CSS with a beautiful, accessible component library built on Reka UI primitives
- ConnectRPC to bridge gRPC to the browser -- type-safe RPC calls from Vue components, generated from the same protos as the backend
- Docker with cargo-chef for deployment -- reproducible builds with excellent layer caching
protos/*.proto <-- Single source of truth for types
|
┌────┴────┐
▼ ▼
Backend Frontend
(Rust) (Nuxt 4)
Tonic ConnectRPC
SQLx Vue 3
Clerk Clerk
Tailwind + shadcn-vue
- Backend: Rust with Tonic gRPC + SQLx/SQLite
- Frontend: Nuxt 4 + Vue 3 + Tailwind CSS v4 + shadcn-vue
- Communication: gRPC-Web via ConnectRPC
- Auth: Clerk (ready to enable)
- Deployment: Docker with cargo-chef caching
# 1. Copy example env files
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# 2. Update backend/.env with your absolute path to the database
# DATABASE_URL=sqlite:///your/path/to/bufstack/backend/data.db
# 3. Install frontend dependencies
cd frontend && bun install && cd ..
# 4. Generate TypeScript types from protos
cd frontend && bun run generate && cd ..
# 5. Start backend, frontend, and worker
bun run devThis starts all three processes concurrently: the gRPC backend, the Nuxt frontend, and the sample background worker. The backend runs on http://localhost:50060 (gRPC) and the frontend on http://localhost:3000. No Clerk account is needed -- auth is disabled by default (see Enabling Auth).
Bufstack uses Vitest for frontend unit tests:
cd frontend
bun run test # Watch mode
bun run test:run # Single run (CI)Test files live next to their source in __tests__/ directories:
app/pages/__tests__/index.test.ts # tests for app/pages/index.vue
app/components/__tests__/Foo.test.ts # tests for app/components/Foo.vue
ESLint with @nuxt/eslint flat config:
cd frontend
bun run lint # Check for issues
bun run lint:fix # Auto-fix issuesWorkers are long-running Rust binaries for background jobs (queue processing, scheduled tasks, cleanup routines, etc.). They live in backend/workers/ and share the same data and services crates as the gRPC server.
A sample placeholder-worker is included out of the box. It logs a heartbeat every 30 seconds and is started automatically by bun run dev:
# Run the worker standalone
cd backend/workers && cargo run --bin placeholder-worker-
Add a
[[bin]]entry tobackend/workers/Cargo.toml:[[bin]] name = "my-worker" path = "src/my_worker.rs"
-
Create the source file at
backend/workers/src/my_worker.rs. -
Add a dev script to the root
package.jsonand include it in thedevcommand:"dev:my-worker": "cd backend/workers && cargo run --bin my-worker", "dev": "concurrently \"bun run dev:backend\" \"bun run dev:frontend\" \"bun run dev:worker\" \"bun run dev:my-worker\""
bufstack/
├── backend/
│ ├── api/ # gRPC server (Tonic, port 50051)
│ ├── data/ # Database layer (SQLx + SQLite)
│ │ ├── migrations/ # SQLx migration files
│ │ ├── models/ # Rust data models
│ │ └── repositories/ # Database access layer
│ ├── services/ # Business logic
│ ├── io/ # IO utilities
│ └── workers/ # Background workers
├── frontend/
│ ├── app/
│ │ ├── components/ui/ # shadcn-vue components
│ │ ├── composables/useGrpc.ts # gRPC client composable
│ │ ├── gen/ # Generated protobuf TypeScript
│ │ ├── lib/utils.ts # Tailwind class merge utility (cn)
│ │ └── pages/ # Vue pages
│ └── server/api/rpc/ # gRPC proxy route
├── protos/ # Protocol Buffer definitions (source of truth)
├── scripts/ # Build & deploy scripts
└── .claude/skills/ # Claude Code skills (e.g. scaffold-entity)
Bufstack uses shadcn-vue for UI components -- accessible, composable primitives built on Reka UI and styled with Tailwind CSS. Browse the full component library at shadcn-vue.com/docs/components.
These components are included out of the box:
| Component | Usage |
|---|---|
Button |
Primary actions, form submits, links |
Card |
Content containers with header/content/footer sections |
Badge |
Labels, tags, status indicators |
Alert |
Inline messages, success/error feedback |
Input |
Text inputs |
Label |
Form labels |
Separator |
Visual dividers |
Components are installed on-demand -- you only add what you use:
# Add a single component
cd frontend && bunx shadcn-vue@latest add dialog
# Add multiple components at once
cd frontend && bunx shadcn-vue@latest add select tooltip dropdown-menuComponents are installed to frontend/app/components/ui/ and are auto-imported by Nuxt. Use them directly in templates:
<template>
<Button variant="outline" size="sm">Click me</Button>
</template>Dark mode is handled via @nuxtjs/color-mode with system preference detection. The theme uses CSS variables (neutral base color, new-york style) defined in app/assets/css/tailwind.css.
New entities should be scaffolded using the /scaffold-entity Claude Code skill. This is the recommended way to add new database-backed entities with full CRUD operations.
The workflow:
-
Create a migration file in
backend/data/migrations/:-- 20260208000000_your_entity.up.sql CREATE TABLE your_entity ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) );
-
Run
/scaffold-entityin Claude Code. It will automatically generate:- Proto definition (
protos/your_entity.proto) -- gRPC service with Create/Get/List/Update/Delete RPCs - Rust model (
backend/data/src/models/your_entity.rs) -- SQLx-compatible struct withFromRow - Repository (
backend/data/src/repositories/your_entity_repository.rs) -- Full CRUD database operations - gRPC service (
backend/api/src/services/your_entity_service.rs) -- Tonic service implementation - Auto-registration in
grpc.rs,build.rs, and allmod.rsfiles - Frontend test UI on
_testing.vue(dev-only CRUD page)
- Proto definition (
-
Run
cargo checkto validate, thencd frontend && bun run generatefor TypeScript types.
If you need a service that doesn't follow the standard entity CRUD pattern:
- Define your service in
protos/your_service.proto - Add the proto to
backend/api/build.rs:tonic_prost_build::compile_protos("../../protos/your_service.proto")?;
- Create
backend/api/src/services/your_service.rs - Register in
backend/api/src/grpc.rs(add import, init, and.add_service()) - Regenerate types:
cd frontend && bun run generate
Auth is disabled by default so new clones can run immediately without a Clerk account. To enable:
- Set
NUXT_PUBLIC_CLERK_ENABLED=trueinfrontend/.env - Uncomment and fill in the Clerk keys in
frontend/.env(seefrontend/.env.examplefor the full template):NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_... NUXT_CLERK_SECRET_KEY=sk_... - Uncomment the auth interceptor lines in
backend/api/src/grpc.rs
When NUXT_PUBLIC_CLERK_ENABLED is absent or not "true", the Clerk module is not loaded, auth pages show a friendly "not configured" message, and gRPC calls work without authentication.
The auth middleware extracts user_id from Clerk JWTs and injects it into gRPC request metadata, making it available to all service implementations.
docker compose upThis starts both bs-backend (port 50051) and bs-frontend (port 3000) with a shared bs-network bridge and a bs-data volume for the SQLite database.
To add a new service (e.g., a worker, a cache, a separate microservice):
-
Add to
docker-compose.yml(development):services: # ... existing services ... bs-your-service: build: context: . dockerfile: path/to/Dockerfile container_name: bs-your-service environment: - RUST_LOG=info - DATABASE_URL=sqlite:///app/data/bufstack.db volumes: - bs-data:/app/data # Share the database volume if needed networks: - bs-network # Same network so services can talk to each other depends_on: - bs-backend # If it depends on the backend restart: unless-stopped
-
Add to
docker-compose.prod.yml(production):bs-your-service: image: ghcr.io/danwritecode/bs-your-service:latest env_file: - "/env/bufstack.env" container_name: bs-your-service networks: - bs-network restart: unless-stopped
-
Update
scripts/build-and-push.shto build and push the new image:docker buildx build \ --platform linux/amd64 \ -f path/to/Dockerfile \ -t $REGISTRY/bs-your-service:$VERSION \ -t $REGISTRY/bs-your-service:latest \ --push \ .
-
If it needs a new port exposed, update
scripts/setup-droplet.shto allow it through the firewall:sudo ufw allow YOUR_PORT/tcp
# Build images and push to GitHub Container Registry
./scripts/build-and-push.sh
# SSH into your server, then:
./scripts/deploy.sh# Run once on a fresh droplet
./scripts/setup-droplet.shThis installs Docker, configures the firewall (SSH, HTTP, HTTPS, port 3000), and sets up fail2ban.