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-devtwice is safe. - You can run it after every
docker compose up -dwithout 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 |
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-1— Brand Refresh (epic, high priority)PROJ-5— Design new logo (story underPROJ-1, in progress, assigned toadmin)INFRA-1— CI/CD Pipeline (epic, high priority)INFRA-4— Backend test workflow (story underINFRA-1, done, assigned toadmin)
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.
The target:
- Validates that
DATABASE_URLends in_devor_test. Aborts otherwise. - Connects to the
postgresdatabase on the same host and runsDROP DATABASE IF EXISTSfollowed byCREATE DATABASE. - Runs
alembic upgrade head. - Runs
make seed-devagainst 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. Runmake seed-devagainst 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 → Webhooksif 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.