git clone https://github.com/<owner>/plexus.git
cd plexus
bun installRunning bun install automatically installs git hooks via Lefthook (the prepare script runs lefthook install). No extra setup is needed.
Plexus uses Drizzle ORM with dual-dialect support (SQLite and PostgreSQL). Schema definitions live in:
packages/backend/drizzle/schema/sqlite/-- SQLite table definitionspackages/backend/drizzle/schema/postgres/-- PostgreSQL table definitions
- Edit the schema
.tsfiles in the appropriate dialect directories - If adding a new table, update the index exports in
packages/backend/drizzle/schema/index.ts - (Optional) Validate locally by running
bun run generate-migrationsfrompackages/backend/-- this confirms your schema produces valid migration SQL. The name is auto-derived from your branch; you can also specify--name <descriptive-name>explicitly. - Do NOT commit migration artifacts (
.sqlfiles,meta/_journal.json, snapshot files). Only commit the schema.tsfiles. - Open your PR. Migrations are auto-generated by CI after your PR merges to
main.
Drizzle migrations use sequential numbering. When multiple PRs include migrations, they collide on merge (e.g., two PRs both generate migration 0030). By keeping migration generation as a post-merge CI step, conflicts are impossible.
The pre-commit hook prevents accidentally committing migration files. If it fires:
# Remove migration files from your staged changes
git reset HEAD -- packages/backend/drizzle/migrations/ packages/backend/drizzle/migrations_pg/Then commit again with only your schema .ts files.
The hooks are skipped automatically when CI=true (GitHub Actions). To skip locally in exceptional circumstances:
LEFTHOOK=0 git commit -m "..."The project uses separate Drizzle ORM config files for each database dialect:
drizzle.config.sqlite.ts-- SQLite configurationdrizzle.config.postgres.ts-- PostgreSQL configuration
When running Drizzle Kit commands, specify the appropriate config file with --config.
The system prompt for the /pi AI agent lives at .github/prompts/pi-assistant.md.
Edit that file directly — do not put prompt text inside the workflow YAML.
The file supports {{dot.notation.path}} placeholders that are substituted at runtime:
{{context.payload.comment.body}}— the triggering comment's text{{context.payload.issue.number}}— issue/PR number{{context.actor}}— the GitHub actor who triggered the run{{env.GITHUB_SHA}}— anyGITHUB_*/RUNNER_*runner environment variable{{env.INITIAL_COMMENT_ID}}— a value passed explicitly via the step'senv:block
Anything reachable from the @actions/github context object
is available under context.* without any extra wiring. Values that come from
previous step outputs (like INITIAL_COMMENT_ID) must be added to the env: block
on the Run Pi agent step in .github/workflows/pi-assistant.yml.
All code must be formatted with Biome before committing:
bun run formatFrom the repo root:
bun run testOr from the backend package:
cd packages/backend
bun run testNote:
bun testis intentionally blocked both at repo root and inpackages/backend. Usebun run testinstead.
The pre-commit hook runs backend tests automatically.
bun run devThis starts the backend (with file watching) and the frontend builder in parallel.
The port is derived automatically from the worktree directory name, so two worktrees can run simultaneously without collision. The port and database are printed at startup:
Starting Plexus Dev Stack...
PORT: 14641
DATABASE_URL: sqlite:///tmp/plexus-browsertesting.db
ADMIN_KEY: password
Override any of these with environment variables:
PORT=4000 ADMIN_KEY=mysecret bun run devTo run against an in-process PGlite database instead of SQLite — useful when testing Postgres-specific behaviour without running a real server:
bun run dev:pgliteThe startup output shows the data directory instead of a database URL:
Starting Plexus Dev Stack...
PORT: 14641
DB Driver: PGlite
DB Data Dir: /tmp/plexus-browsertesting.pglite
ADMIN_KEY: password
The data directory persists across restarts (same worktree isolation as SQLite).
Override it with PLEXUS_PGLITE_DATA_DIR=/path/to/dir bun run dev:pglite.
After starting the dev server, seed it with a realistic baseline configuration (providers, model aliases, quota definitions, and API keys) that exercises the full feature set without requiring any real external credentials:
bun run populate-devThis is idempotent — safe to re-run at any time. It uses PUT throughout, so
existing resources are replaced rather than duplicated.
| Category | Count | Notes |
|---|---|---|
| Providers | 7 | Local (Ollama, LM Studio, llama.cpp) + mock cloud (OpenAI, Anthropic, Gemini, OpenRouter) |
| Quotas | 7 | Rolling, daily, weekly windows; requests and token limits |
| Model aliases | 16 | chat, embeddings, speech, transcriptions, image types; multi-target failover aliases |
| API keys | 14 | Unrestricted, quota-enforced, provider-restricted, model-restricted |
All provider URLs point at localhost — no real API keys are needed by default.
Create scripts/user-populate.json (git-ignored — see scripts/user-populate.example.json
for the format). Anything in that file is merged over the defaults when you run
bun run populate-dev, so you can add real provider keys or personal aliases
without touching committed files and without risk of accidentally leaking secrets.
| Variable | Default | Purpose |
|---|---|---|
PLEXUS_URL |
http://localhost |
Base URL of the target instance |
PLEXUS_PORT |
derived from cwd | Port (matches bun run dev automatically) |
PLEXUS_ADMIN_KEY |
password |
Admin key |
To wipe the database and restart the backend in one step:
bun run clear-devThis deletes the SQLite file from /tmp and sends SIGHUP to the running dev
server, which gracefully shuts down the backend and immediately relaunches it
against the empty database. The frontend is unaffected. The whole cycle takes
about two seconds.
After clearing, re-run bun run populate-dev to restore the baseline config.
Note:
clear-devrelies on a PID file written bybun run devto/tmp/plexus-<worktree>.pid. If the server is not running, it will delete the database file and exit cleanly without error.