For Players: Visit www.csheet.net to create and manage your D&D characters.
For Developers: This README contains instructions for setting up a local development environment and self-hosting CSheet.
CSheet is an open-source, self-hostable D&D 5th Edition character sheet application that supports both the 2014 (SRD 5.1) and 2024 (SRD 5.2) rulesets.
What CSheet Is:
- A companion tool for managing your D&D characters
- Support for tracking ability scores, skills, spells, hit points, and progression
- Open-source software you can run on your own server
- Designed for use alongside traditional tabletop play
What CSheet Is NOT:
- A virtual tabletop (VTT) platform
- A dice rolling simulator
- A replacement for sitting at the table with your friends
CSheet is designed to help you track your character while you roll your own physical dice and engage with your fellow players around the table (or video call).
This project uses mise for managing tools and dependencies.
To install mise, visit the mise installation guide.
Once mise is installed, run:
mise installThis will install all required tools including:
- Bun - Fast JavaScript runtime and bundler
- dbmate - Database migration tool
- jj - Version control
- jq - JSON processor
Install dependencies:
mise run installStart Docker services (PostgreSQL and MinIO):
mise run deps:upRun database migrations:
mise run db:upgradeStart the development server:
mise run app:devThe app:dev task automatically starts Docker services if they're not running.
The application will be available at http://localhost:3000.
This project uses mise run for all common tasks:
| Command | Description |
|---|---|
mise run app:dev |
Start development server with hot reload |
mise run app:prod |
Start production server |
mise run app:container |
Build and run the app inside the local Docker Compose stack |
mise run deploy:push |
Build and push the application image to Artifact Registry |
mise run infra:preview |
Preview infrastructure changes (VPC, Cloud SQL, secrets, service accounts, Artifact Registry) |
mise run infra:up |
Apply infrastructure changes |
mise run infra:destroy |
Destroy the infrastructure stack |
mise run deploy:preview |
Preview Cloud Run service and migration job changes |
mise run deploy:up |
Deploy the Cloud Run service and migration job |
mise run deploy:destroy |
Remove the Cloud Run service and migration job |
mise run install |
Install dependencies with bun |
mise run test |
Run all tests with test database |
mise run deps:up |
Start all Docker services (PostgreSQL and MinIO) |
mise run deps:down |
Stop all Docker services |
mise run db:upgrade |
Run database migrations |
mise run dbmate <command> |
Run dbmate commands (see Database section) |
mise run db:psql |
Open PostgreSQL shell for the database |
mise run check |
Run Biome linting/formatting checks and TypeScript validation |
mise run check-fix |
Auto-fix linting/formatting issues and check types |
The containerized app uses the app compose profile. Stop it (and supporting services) with docker compose --profile app down.
CSheet uses PostgreSQL 16 for data storage with dbmate for schema migrations.
Migrations are stored in the migrations/ directory. Each migration file is timestamped and contains SQL to modify the database schema.
Common dbmate commands:
# Apply all pending migrations
mise run db:upgrade
# Create a new migration
mise run dbmate new create_something
# Rollback the last migration
mise run dbmate down
# Show migration status
mise run dbmate statusTests use a separate csheet_test database on the same PostgreSQL instance.
The test database is automatically created and migrated when you run:
mise run testEach test runs inside a PostgreSQL transaction that is rolled back after the test completes, ensuring a clean database state between tests.
The database includes tables for:
- users - User authentication
- characters - Character base info (name, species, background, ruleset)
- character_levels - Character class levels and progression
- character_abilities - Ability scores (STR, DEX, CON, INT, WIS, CHA)
- character_skills - Skill proficiencies and modifiers
- character_hp - Hit points tracking
- character_hit_dice - Hit dice tracking
- character_spell_slots - Spell slot tracking
- character_spells_learned - Spellbook/known spells
- character_spells_prepared - Prepared spells
Open an interactive PostgreSQL shell:
mise run db:psqlCSheet is built with modern web technologies focused on server-side rendering and progressive enhancement:
- Hono - Lightweight web framework
- JSX Server-Side Rendering - Components rendered on the server
- htmx - Client-side interactions without writing JavaScript
- Bootstrap 5 - CSS framework for styling
- Bun - Fast all-in-one JavaScript runtime
All pages are rendered server-side using JSX components. The Hono framework provides JSX rendering out of the box, allowing you to write components that look like React but render to HTML on the server.
Instead of a heavy JavaScript framework, CSheet uses htmx for dynamic interactions. htmx allows you to:
- Submit forms without page reloads
- Update parts of the page with server responses
- Trigger actions with minimal JavaScript
Example: Updating a character's ability score sends a POST request and the server returns updated HTML that htmx swaps into the page.
csheet/
├── src/
│ ├── app.ts # Main application, registers routes and middleware
│ ├── config.ts # Configuration (database path, ports, etc.)
│ ├── db.ts # Database connection
│ ├── middleware.ts # Middleware registration
│ ├── components/ # JSX components for rendering pages
│ │ ├── Layout.tsx # Main layout wrapper with navbar
│ │ ├── Welcome.tsx # Home page
│ │ ├── ...
│ │ └── ui/ # Reusable UI components
│ ├── routes/ # Route handlers
│ │ ├── index.tsx # Home route (/)
│ │ ├── auth.tsx # Authentication routes
│ │ ├── character.tsx # Character CRUD and update routes
│ │ └── spells.tsx # Spell reference routes
│ ├── middleware/ # Middleware
│ │ ├── auth.ts # Authentication middleware
│ │ ├── flash.ts # Flash messages
│ │ └── cachingServeStatic.ts # Static file serving with caching
│ ├── db/ # Database models and queries
│ │ ├── users.ts
│ │ ├── char_hp.ts
│ │ └── ...
│ ├── services/ # Business logic
│ │ ├── createCharacter.ts
│ │ ├── computeCharacter.ts # Compute derived stats
│ │ ├── longRest.ts
│ │ └── ...
│ └── lib/ # Utilities and helpers
│ ├── dnd/ # D&D rules engine
│ │ ├── rulesets.ts # Ruleset loader
│ │ ├── srd51.ts # D&D 5e SRD 5.1 rules
│ │ └── srd52.ts # D&D 5e SRD 5.2 rules
│ ├── schemas.ts # Zod validation schemas
│ └── ...
├── migrations/ # Database migrations
├── db/ # Database utilities
│ ├── init.sql # SQLite initialization
│ └── schema.sql # Full schema dump
├── static/ # Static assets (CSS, images, etc.)
├── mise.toml # mise configuration
└── main.ts # Application entry point
CSheet supports multiple D&D 5th edition rulesets:
- SRD 5.1 - D&D 5th Edition System Reference Document v5.1
- SRD 5.2 - D&D 5th Edition System Reference Document v5.2 (2024 rules)
The ruleset system is pluggable and defined in src/lib/dnd/:
rulesets.ts- Ruleset loader and interfacesrd51.ts- SRD 5.1 class definitions, spells, species, etc.srd52.ts- SRD 5.2 class definitions, spells, species, etc.
Each character is associated with a ruleset, allowing players to use either the 2014 or 2024 rules. The ruleset determines:
- Available species (races)
- Class features and progressions
- Spell lists
- Ability score calculations
- Proficiency bonuses
CSheet uses Bun's built-in test runner for testing with a comprehensive integration testing setup.
Run all tests:
mise run testRun specific test file:
mise run test src/routes/character.test.ts- Test Database: Uses separate
csheet_testdatabase - Transaction Isolation: Each test runs in a transaction that auto-rolls back
- Factory Pattern: Test fixtures created with fishery and @faker-js/faker
- HTML Testing: Server-rendered HTML validated with linkedom
- Integration Tests: Full-stack tests through HTTP requests (not unit tests)
Tests use RSpec-style nested describe blocks with fixtures:
import { describe, test, expect, beforeEach } from "bun:test"
import { useTestApp } from "@src/test/app"
import { userFactory } from "@src/test/factories/user"
import { makeRequest, parseHtml, expectElement } from "@src/test/http"
describe("GET /characters", () => {
const testCtx = useTestApp()
describe("when user is authenticated", () => {
let user: User
beforeEach(async () => {
user = await userFactory.create({}, testCtx.db)
})
test("displays the character list", async () => {
const response = await makeRequest(testCtx.app, "/characters", { user })
const document = await parseHtml(response)
const title = expectElement(document, "title")
expect(title.textContent).toContain("My Characters")
})
})
})See src/routes/character.test.ts for a complete example.
The project uses Biome for linting and formatting, and TypeScript for type checking.
Run checks:
mise run checkAuto-fix issues:
mise run check-fixSee CLAUDE.md for detailed development guidelines including:
- Using Bun instead of Node.js
- Hono framework patterns
- Component structure
- Authentication patterns
- Database conventions
- Testing patterns