Skip to content

Seed data#

The seed script populates a realistic local dataset so you have something to click on while you're learning the system. This page describes exactly what it creates, the credentials it provisions, and how to reset the database.

The script lives at backend/scripts/seed_dev.py. It is dev-only — the credentials are deliberately weak and the script never runs in a production image's startup sequence.

Running the seed#

The Makefile exposes two variants:

# Run inside the running backend container (no Poetry on the host required)
make seed-dev

# Run from a host Poetry virtualenv (requires `cd backend && poetry install`)
make seed-dev-local

The container variant is the one to use right after docker compose up -d. It needs the backend service running and the database reachable from inside the container.

Idempotency#

The script bails out cleanly if the seed has already been applied. The sentinel: the bootstrapped workspace (named after Settings.workspace_name, default Default Workspace) with a human user named admin inside it. If both exist, the script prints [seed_dev] Workspace 'Default Workspace' already seeded — nothing to do. and exits zero.

This means:

  • Running make seed-dev twice is safe.
  • You can run it after every docker compose up -d without thinking.
  • You cannot use it to "add more" seed data — to top up, edit the script.

If you need a true reset, see Resetting the dev database below.

What gets created#

The seed runs eight phases, each committing before moving on. All entity UUIDs are derived deterministically (via uuid.uuid5 in a fixed namespace), so the same make seed-dev on two different hosts produces identical IDs. That makes hard-coded references in docs and screenshots stable.

Workspace#

Field Value
Name Settings.workspace_name (default Default Workspace)

The workspace is auto-created on first boot of the backend regardless of seeding (workspace bootstrap is part of the backend lifespan). The seed script reads the same Settings.workspace_name value, so it attaches to whichever workspace the backend bootstrapped — overriding WORKSPACE_NAME in .env is supported, but you must do it before first boot. Subsequent rows attach to that workspace.

Human user#

Field Value
Username admin
Display name Admin
Email admin@example.com
Password changeme-please (bcrypt cost 12)
can_edit_global_wiki true
rate_limit_per_minute 600

The user is created with is_active=true and is the actor credited with all subsequent seeded writes — projects, tickets, wiki pages, and sticky notes are all attributed to admin in the audit trail.

Bots#

Two bots, each with a known plaintext token:

Bot Mode Project allowlist Token
bot-research write PROJ + INFRA tkb_devresearchtoken0000000000000001
bot-reporting read INFRA only tkb_devreportingtoken000000000000001

The tokens are stored as argon2id hashes using the configured parameters, exactly the same as a real bot creation. The plaintext values are written into the script as constants so you can copy them straight from this page into a curl call or an MCP client.

The research bot also has can_edit_global_wiki=true. The reporting bot does not.

Tokens are credentials

These plaintexts are public. They exist for local convenience. Never use them on a host that anything but localhost can reach. Rotate them via the UI (Settings → Bots → Rotate token) or via POST /bots/{id}/tokens/rotate before exposing the host.

Projects#

Key Name Description
PROJ Marketing Site Website redesign and content management.
INFRA Infrastructure Homelab and CI/CD infrastructure.

Each project is created with the default workflow state set (Backlog, Todo, In Progress, Done) and a per-project ticket counter starting at 1. The seed only assigns tickets to Backlog, In Progress, and Done, so the Todo column starts empty.

Tickets#

31 tickets are distributed across the two projects:

Project Epics Stories Subtasks
PROJ 4 8 (2 per epic) 5
INFRA 3 6 (2 per epic) 5

Tickets are spread across Backlog, In Progress, and Done. Some are assigned to admin, some to bot-research, and some are unassigned. Priorities cover low, medium, and high. The hierarchy is real — epic → story → subtask parent links resolve correctly, so the ticket detail page renders parent/child sections immediately.

Sample tickets you'll see:

  • PROJ-1Brand Refresh (epic, high priority)
  • PROJ-5Design new logo (story under PROJ-1, in progress, assigned to admin)
  • INFRA-1CI/CD Pipeline (epic, high priority)
  • INFRA-4Backend test workflow (story under INFRA-1, done, assigned to admin)

Exact display IDs depend on insertion order; the per-project counter is monotonic and starts at 1.

Wiki pages#

Five pages with cross-links — four resolved, one deliberately broken:

Slug Scope Notes
getting-started PROJ Links to design-system and content-guide. Demonstrates a forward reference.
design-system PROJ Links back to getting-started. Created after it, so the original's wikilink resolves retroactively.
content-guide PROJ Links to design-system.
infra-overview INFRA Links to monitoring-setup — a page that is not created. The link stays unresolved and renders as a broken-link badge.
team-handbook Global (no project) Workspace-level reference page.

The retroactive-resolution flow is intentional: getting-started is created first with a reference to [[design-system]]. At write time, the link is stored as unresolved. When design-system lands, the wikilink resolver back-fills the reference and the page renders as a normal link. This mirrors how the production resolver behaves for any new page.

The broken [[monitoring-setup]] link on infra-overview is a teaching example. It is never created, so the page will keep showing a broken-link style next to that phrase until you make a monitoring-setup page yourself.

Sticky notes#

Four notes — two personal (visible only to admin), two project-scoped (visible to anyone in the workspace):

Scope Body
Personal Remember: rotate bot tokens before next release.
Personal Check vendor pricing for analytics by end of month.
PROJ Design review meeting: Friday 2pm. Stakeholder needs logo options.
INFRA Postgres standby is running 2 minor versions behind. Schedule upgrade.

Two of the notes carry a color hint (yellow and green). The other two render with the default tone.

Console output#

A successful seed run prints something like:

[seed_dev] Starting seed for workspace 'Default Workspace' …
[seed_dev] workspace b5e7a2d0-...
[seed_dev] human user 'admin' (b5e7a2d0-...)
[seed_dev] project PROJ b5e7a2d0-...
[seed_dev] project INFRA b5e7a2d0-...
[seed_dev] bot 'bot-research' token: tkb_devresearchtoken0000000000000001
[seed_dev] bot 'bot-reporting' token: tkb_devreportingtoken000000000000001
[seed_dev] created ~31 tickets
[seed_dev] created 5 wiki pages (1 broken link: monitoring-setup)
[seed_dev] created 4 sticky notes
[seed_dev] Done.

The bot token lines are the only place those plaintexts ever appear in operator-visible output (besides this page). In production, tokens are shown exactly once at creation; the seed script bypasses that one-shot model because the tokens are pinned constants.

Resetting the dev database#

The Makefile exposes a reset-db target that drops the database, re-runs migrations, and re-seeds. It refuses to run unless DATABASE_URL ends in _dev or _test, so it cannot wipe a non-dev database by accident.

DATABASE_URL=postgresql+asyncpg://ticket_board:changeme@localhost/ticket_board_dev \
  make reset-db

The target:

  1. Validates that DATABASE_URL ends in _dev or _test. Aborts otherwise.
  2. Connects to the postgres database on the same host and runs DROP DATABASE IF EXISTS followed by CREATE DATABASE.
  3. Runs alembic upgrade head.
  4. Runs make seed-dev against the fresh schema.

This requires three things on the host invoking make: psql (for the drop/create), a working Poetry install (cd backend && poetry install) because the migrate step runs poetry run python -m alembic ... locally, and the docker compose stack running (the chained make seed-dev execs into the backend container). If any of those are missing — or you want to keep everything containerised — the manual sequence is:

docker compose down -v          # destroy the data volume
docker compose up -d             # re-create everything from scratch
make seed-dev                    # populate

down -v is the bigger hammer — it also destroys Caddy's certificate cache. For local plain-HTTP dev that's fine; for any deployment with real ACME certificates it is not.

When the seed is not enough#

The seed is fixed by design — it gives you a representative dataset, not a stress test. If you need more, two patterns work:

  • Edit the script. It's well-commented and the helpers (_create_tickets, _create_wiki_pages, _create_sticky_notes) are easy to extend. Run make seed-dev against a freshly reset database to apply your additions.
  • Use the REST or MCP API. A seeded bot with write access (bot-research) can create tickets, comments, and wiki pages programmatically. This is how the integration tests exercise the bot path; the same approach works for hand-rolling a larger dataset. See the REST API reference and the MCP tool catalog.

What the seed does not create#

By design, the seed leaves these surfaces empty:

  • Webhooks — none. Create one from Settings → Webhooks if you want to exercise the delivery worker.
  • Tool invocation log entries — none until a bot actually calls a tool.
  • Audit events older than the seed run — the audit table only contains the seed's own writes.

The empty webhook table is the most common gotcha. The webhook delivery worker is fully wired, but with zero subscriptions it has nothing to deliver. The webhooks operator guide walks through creating one.