Bunyip (Australian folklore): A lake-dwelling cryptid of Aboriginal stories. Mascot: a shaggy creature with wide, friendly eyes peering through reeds. Tagline: Surfaces what matters.
  • Rust 94.5%
  • Just 1.8%
  • Dockerfile 0.9%
  • CSS 0.9%
  • HTML 0.8%
  • Other 1.1%
Find a file
2026-06-13 19:47:06 +02:00
.cargo feat(oidc): port bunyip-oidc provider on dunite-oidc keys 2026-05-30 13:40:51 -04:00
.forgejo/workflows fix(migrations): close data-integrity gaps and gate version collisions 2026-06-13 07:15:06 -04:00
.idea chore: Add JetBrains .idea preferences 2026-06-12 09:21:51 -04:00
.sqlx feat(oidc): port bunyip-oidc provider on dunite-oidc keys 2026-05-30 13:40:51 -04:00
bunyip-api fix(bunyip-domain): correct repository SQL binding, silent no-ops, and dead methods 2026-06-13 07:22:12 -04:00
bunyip-web Merge branch 'main' into fix/bunyip-78 2026-06-13 13:15:40 +02:00
crates fix(bunyip-domain): correct repository SQL binding, silent no-ops, and dead methods 2026-06-13 07:22:12 -04:00
dev-docs docs: add 2026-06-06 platform audit to dev-docs/audits 2026-06-11 03:31:29 +02:00
docs feat(m1): vas sprint - google oauth + wiring 2026-05-29 14:14:46 +02:00
scripts fix(migrations): close data-integrity gaps and gate version collisions 2026-06-13 07:15:06 -04:00
secrets feat: a8n.tools (menkent) parity - SSR frontend, governance conformance, domain rename 2026-05-30 15:12:22 -04:00
.dockerignore feat: a8n.tools (menkent) parity - SSR frontend, governance conformance, domain rename 2026-05-30 15:12:22 -04:00
.env.example fix(auth): harden middleware and auto-ban against IP spoofing and map growth 2026-06-12 05:58:33 -04:00
.gitignore docs: add 2026-06-06 platform audit to dev-docs/audits 2026-06-11 03:31:29 +02:00
AUDIT.md docs(audit): rewire decisions for the bunyip-on-mokosh-server hub 2026-05-13 05:31:38 +02:00
Cargo.lock feat(feedback): magic-byte MIME validation + dim bomb cap + proxy hardening 2026-06-11 05:52:19 +02:00
Cargo.toml Release v0.2.1 2026-06-08 21:36:09 +02:00
CLAUDE.md fix(toolchain): address PR #39 review findings 2026-06-02 21:24:38 +02:00
compose.dev-sso.yml feat(dev-sso): make bunyip-api the OIDC OP for the hub and mokosh SPA 2026-06-05 13:34:01 -04:00
compose.dev.yml chore(compose): pass through distribution limit/TTL env vars (BUNYIP-42) 2026-06-03 17:15:47 +02:00
compose.yml chore(compose): pass through distribution limit/TTL env vars (BUNYIP-42) 2026-06-03 17:15:47 +02:00
justfile feat(justfile): add dev-clean / dev-clean-all teardown recipes 2026-06-13 12:53:01 -04:00
LICENSE.md chore: MIT license, secret-generating ensure-env, justfile groups, startup banners 2026-05-30 18:00:58 -04:00
README.md fix(release): publish binaries to psa-systems-private Generic Packages 2026-05-28 20:45:17 +02:00
rust-toolchain.toml fix(toolchain): address PR #39 review findings 2026-06-02 21:24:38 +02:00

bunyip

Bunyip (Australian folklore): A lake-dwelling cryptid of Aboriginal stories. Mascot: a shaggy creature with wide, friendly eyes peering through reeds. Tagline: Surfaces what matters.

Bunyip is the front-facing SaaS / business platform for the Mokosh PSA product. It owns marketing, signup, login (eventually via OIDC against Mokosh Server), organization onboarding, subscription billing UI, and platform admin.

Status

Pre-MVP. The current iteration ships a fully-wired frontend backed by seeded JSON data; every interactive element works, but state mutations land in an in-memory mock store rather than a real database. Real backend functionality (auth crypto, OIDC issuance, Stripe, persistence) is post-MVP and will live in Mokosh Server, not in Bunyip.

See For AI/bunyip-mvp-plan.md for the full MVP scope and For AI/bunyip-progress.md for live progress.

Architecture (target)

Domain Service
a8n.systems Bunyip (this repo): SaaS / business / billing
msp.a8n.systems Mokosh Client: actual PSA application
api.a8n.systems Mokosh Server: headless API + OIDC issuer (post-MVP)

Stack:

  • Rust + Axum (thin backend; serves the SPA + mock JSON endpoints)
  • Rust + Dioxus (frontend; no Node.js dev server)
  • parking_lot::RwLock + JSON seeds for the in-memory mock store (MVP only; real persistence moves to Mokosh Server later)

Quickstart (dev)

Requires Docker / Podman with Compose.

docker compose --file compose.dev.yml up --detach

Then visit:

State resets on container restart. This is intentional for the MVP demo loop.

Self-host (production)

compose.yml is the reference deployment. It runs the published OCI images (no build-from-source) behind an edge Caddy that terminates TLS and routes traffic.

Topology

The Dioxus SPA loads its OIDC config at runtime from a same-origin /config.json (see OIDC runtime config below) and, when the issuer is left unset, derives it from the host as msp-api.<window.location.host> (see bunyip-web/src/stores/config.rs). So a deployment needs two DNS names pointing at the host:

  • ${BUNYIP_HOST} - serves the SPA (e.g. bunyip.example.com)
  • msp-api.${BUNYIP_HOST} - serves the API / OIDC issuer (e.g. msp-api.bunyip.example.com)

The edge Caddy (oci-build/Caddyfile) issues Let's Encrypt certs for both, proxying the apex to the web container and the msp-api. subdomain to the api container.

OIDC runtime config

The bunyip-web SPA is environment-agnostic: one image serves any deployment. At container start its entrypoint (bunyip-web/oci-build/entrypoint.sh) writes a same-origin /config.json from the container's environment, and Caddy serves it with Cache-Control: no-store. The SPA fetches it once at boot before any auth UI renders. Changing a value and restarting the container reconfigures OIDC with no rebuild.

Set these on the web service (all OIDC values are public by design - a public client + PKCE):

Env var Required Fallback when unset
BUNYIP_OIDC_CLIENT_ID yes (for SSO) none
BUNYIP_OIDC_ISSUER no host-derived https://msp-api.<host>
BUNYIP_OIDC_REDIRECT_URI no <window.location.origin>/auth/callback
BUNYIP_OIDC_SCOPES no openid email offline_access
// GET /config.json
{
  "issuer": "https://msp-api.bunyip.example.com",
  "client_id": "00000000-0000-0000-0000-000000000000",
  "redirect_uri": "",
  "scopes": "openid email offline_access"
}

The dx serve dev loop (just dev-sso) runs a different dev server that cannot serve a root /config.json, so under hot-reload the SPA uses the host-derived issuer/redirect and does not receive client_id. For end-to-end SSO testing locally, run the production image.

Deploy

The images live in the private dev.a8n.run/psa-systems-private registry, so authenticate before pulling:

docker login dev.a8n.run
cp .env.example .env
# Edit .env: set BUNYIP_HOST, COOKIE_SECRET (64+ random chars), CADDY_ACME_EMAIL.
docker compose up --detach

Pin a specific release instead of :latest by setting BUNYIP_API_IMAGE / BUNYIP_WEB_IMAGE to a tagged image (e.g. dev.a8n.run/psa-systems-private/bunyip-api:v0.2.0).

Update checking

The instance reports its version and whether a newer release is published:

http get https://msp-api.${BUNYIP_HOST}/version
{
  "version": "0.1.0",
  "revision": "<git sha>",
  "update": {
    "enabled": true,
    "current": "0.1.0",
    "latest": "0.2.0",
    "update_available": true,
    "checked_at": "2026-05-22T12:00:00+00:00"
  }
}

The check polls BUNYIP_UPDATE_CHECK_URL (defaults to the public Forgejo releases/latest endpoint) at most once an hour and caches the result. Set the URL to an empty string to disable checking (update.enabled becomes false).

Applying an update

Updates are never automatic; the operator decides when to apply one:

docker compose pull
docker compose up --detach

Pinned-tag deployments bump the tag in .env first, then run the same two commands.

Architectures

Images publish for linux/amd64 by default. To publish a multi-arch manifest, set the CI variable BUNYIP_BUILD_PLATFORMS to linux/amd64,linux/arm64. Both Dockerfiles are arch-portable; arm64 builds run under emulation unless a native arm64 runner is available.

Without Docker (Linux x86_64)

Every release publishes binary tarballs to the psa-systems-private/bunyip Generic Packages registry (same org as the OCI images) for operators who want to run Bunyip without Docker:

  • bunyip-api-vX.Y.Z-x86_64-linux-musl.tar.gz - statically-linked bunyip-api + seeds/. Runs on any Linux distribution (musl-static, no glibc dependency).
  • bunyip-web-vX.Y.Z-static.tar.gz - the built public/ directory (WASM bundle + assets). Serve with any static web server.
  • SHA256SUMS - checksums for the two tarballs.

The registry is private, so download requires a Forgejo token with read:package scope (the same kind of token you already use for docker login dev.a8n.run):

let tag = "v0.1.1"
let base = $"https://dev.a8n.run/api/packages/psa-systems-private/generic/bunyip/($tag)"
let token = "<forgejo token, read:package scope>"
let auth = {Authorization: $"token ($token)"}
http get --headers $auth $"($base)/SHA256SUMS" | save SHA256SUMS
http get --headers $auth $"($base)/bunyip-api-($tag)-x86_64-linux-musl.tar.gz" | save $"bunyip-api-($tag)-x86_64-linux-musl.tar.gz"
http get --headers $auth $"($base)/bunyip-web-($tag)-static.tar.gz" | save $"bunyip-web-($tag)-static.tar.gz"
sha256sum --check SHA256SUMS
tar --extract --gzip --file $"bunyip-api-($tag)-x86_64-linux-musl.tar.gz"
tar --extract --gzip --file $"bunyip-web-($tag)-static.tar.gz"

Run the API directly; configure it with the same env vars used in compose.yml (BUNYIP_PUBLIC_BASE_URL, COOKIE_SECRET, BUNYIP_SEEDS_DIR, etc.):

cd $"bunyip-api-($tag)-x86_64-linux-musl"
$env.COOKIE_SECRET = "<64+ random chars>"
$env.BUNYIP_PUBLIC_BASE_URL = "https://bunyip.example.com"
$env.BUNYIP_SEEDS_DIR = "./seeds"
./bunyip-api

Serve the SPA bundle from any static host (Caddy, nginx, GitHub Pages, etc.). The entrypoint that writes /config.json from env is Docker-only, so for the binary path either set BUNYIP_OIDC_* via a hand-written public/config.json before serving, or rely on the host-derived defaults documented in OIDC runtime config.

Only x86_64-linux-musl ships today. ARM64 binaries are gated on the same native runner that gates multi-arch OCI images (see Architectures).

Seeded demo accounts

All accounts accept MOCK_PASSWORD (default demo). When MFA is enabled, TOTP step accepts MOCK_TOTP_CODE (default 000000) or any 6-digit code.

Email Role Org membership Subscription tier Purpose
admin@a8n.systems platform admin - - Access to /admin/*
owner@example.com member Owner of "Example MSP" early_adopter Primary demo account
pastdue@example.com member Owner of "Acme Tech" past_due Dunning banner demo
member@example.com member Member of "Example MSP" inherits org tier Member-permission demo
lifetime@a8n.systems member Owner of "Lifetime LLC" lifetime Lifetime-tier UI

Documentation

Project context, plans, audits, and architecture decisions live in For AI/:

Contributing

  • Work happens in feat/ / fix/ / chore/<short-descriptive-name> branches off main.
  • Merge target is main (via PR).
  • Dev container naming follows dev-bunyip-<service>-${USER} and network dev-bunyip-private-${USER}; the production stack (compose.yml) drops the dev- prefix.
  • Releases: just create-release <major|minor|hotfix> bumps the workspace version and opens a release PR; merging it tags vX.Y.Z and publishes the images (see .forgejo/workflows/create-release.yml).