- Python 56%
- TypeScript 28.2%
- Vue 14%
- CSS 0.7%
- Shell 0.4%
- Other 0.6%
|
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 57s
CI / pre-commit (pull_request) Successful in 47s
test-build / build (backend) (pull_request) Successful in 58s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m3s
CI / e2e (pull_request) Successful in 2m9s
CI / backend-test (pull_request) Successful in 2m14s
CI / release-scripts (push) Successful in 5s
security-scan / SBOM (trivy) (push) Successful in 11s
security-scan / Filesystem scan (trivy fs) (push) Successful in 14s
security-scan / JS SCA (npm audit) (push) Successful in 16s
security-scan / Python SAST (bandit) (push) Successful in 17s
security-scan / Python SCA (pip-audit) (push) Successful in 34s
CI / pre-commit (push) Successful in 48s
publish-main / build (frontend) (push) Successful in 59s
publish-main / build (backend) (push) Successful in 1m3s
publish-main / build (push) Successful in 0s
CI / frontend-test (push) Successful in 1m4s
CI / e2e (push) Successful in 2m9s
CI / backend-test (push) Successful in 2m17s
# Conflicts: # CHANGELOG.md # docs/wdr/0004-generated-api-types.md |
||
|---|---|---|
| .claude | ||
| .forgejo | ||
| backend | ||
| containers | ||
| docs | ||
| frontend | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .pre-commit-config.yaml | ||
| CHANGELOG.md | ||
| CODE_OF_CONDUCT.md | ||
| compose.dev.yaml | ||
| CONTRIBUTING.md | ||
| justfile | ||
| LICENSE | ||
| publiccode.yml | ||
| README.md | ||
| renovate.json | ||
| SECURITY.md | ||
Waggle
A personal command centre for saved Mattermost messages.
EXPERIMENTAL - NOT PRODUCTION READY
Waggle is an individual experiment by an employee of the Dutch central government, published as open source for learning, sharing and discussion.
It is known not to meet (yet) the requirements and standards that the Dutch government sets for production software, including but not limited to:
- Information security: no BIO/ISO 27001 assessment (BIO is the Dutch government baseline for information security), no penetration test, no formal threat-model review.
- Privacy: no DPIA carried out; not assessed for GDPR suitability. Not suitable for processing special-category or otherwise sensitive personal data.
- Accessibility: not tested against WCAG 2.2 / EN 301 549.
- Standards: not assessed against the NeRDS guidelines or the "comply or explain" standards of Forum Standaardisatie.
- Operations: no formal maintenance, no SLA, no support channel, no continuity guarantee. It may break or stop without notice.
- Architecture: single-user, local deploy. No scalability, no high availability, no audit logging, no role-based access control.
Use at your own risk and for exploratory purposes only. Contributions, feedback and issues are very welcome - this is a learning environment.
What does Waggle do?
- Imports your saved (flagged) Mattermost posts into a Focus view with five sections (Doe nu / Nieuw / Backlog / Bubble up in de toekomst / Voltooid) plus a separate reading list.
- Completing a card automatically unsaves the message in Mattermost (with retry/backoff).
- An external unsave in Mattermost automatically completes the card: one mental model, one source of truth.
- Rich preview per card: markdown body, attachments (images in a lightbox, files as chips), author avatar, thread context, notes.
- Bubble-up: defer a card to a moment in the future.
- Blocked flag: mark that you are waiting on someone else.
- A "Nieuw" badge on cards you have not looked at yet, dark/light theme, full keyboard navigation.
Note that the user interface is in Dutch; the section names above are the literal labels in the app.
Architecture
- Backend: Python 3.14 + FastAPI + SQLAlchemy async + Postgres (dev and prod). Tokens encrypted at rest with Fernet (HKDF domain-separated key). Auth via a signed session cookie. Rate limiting + Origin check as CSRF protection.
- Frontend: Vue 3.5 (Composition API +
<script setup>) + Vite + TypeScript + TanStack Vue Query + Vue Router. NLDD design system (Lit web components + design tokens, OKLCH, dark/light). - Sync: an asyncio loop inside the FastAPI lifespan, concurrency guarded by
an
asyncio.Lock. Default interval 5 min (configurable 1-60 min).
Dev quickstart
Requires: Postgres running (see "Local dev stack" below) and
WAGGLE_DATABASE_URL set in .env. The backend has no fallback database and
will not start without it.
# Backend
cd backend
uv sync
uv run waggle generate-secrets # → WAGGLE_SECRET_KEY=...
# Put the value in a local .env file next to pyproject.toml.
# Login goes through OIDC (start Keycloak via compose.dev.yaml) or through the
# dev-only synthetic-auth fallback (WAGGLE_DEV_SYNTHETIC_AUTH=1).
uv run uvicorn waggle.main:app --reload --port 8000
# Frontend (separate terminal)
cd frontend
npm install
npm run dev # → http://localhost:5173
Or everything through the justfile:
just dev-backend # backend with reload on :8000
just dev-frontend # vite dev server on :5173
just preview # builds the frontend + starts the backend serving static on :8090
just urls
Local dev stack: Keycloak + Postgres
compose.dev.yaml starts two services for local development:
- Keycloak (port 8082) - OIDC provider with the
waggle-devrealm preloaded. Replaces the external ZAD Keycloak so you can work offline. - Postgres 17 (port 5432) - the same engine that runs on ZAD. Required: the
backend has no fallback database and will not start without
WAGGLE_DATABASE_URL.
We use podman compose (same compose spec as docker, no daemon required).
docker compose works too.
# Start the whole stack (Keycloak + Postgres)
podman compose -f compose.dev.yaml up -d
# Or just Postgres (needed to start the backend or run the tests)
podman compose -f compose.dev.yaml up -d postgres
# By default Keycloak binds to localhost:8082 and Postgres to localhost:5432.
# Different host or port? Set KC_HOSTNAME, KC_PORT or PG_PORT as shell env vars.
# The dev realm uses wildcard redirect URIs, so extra hosts work without
# changing the realm; KC_HOSTNAME only steers the issuer claim in tokens.
KC_PORT=8085 PG_PORT=15432 podman compose -f compose.dev.yaml up -d
Running the backend against Postgres:
# In backend/.env
WAGGLE_DATABASE_URL=postgresql+asyncpg://waggle:waggle@localhost:5432/waggle
Alembic applies the schema automatically on first startup (see
backend/alembic/).
Keycloak is available once its healthcheck passes (~30-60 sec):
| URL (default) | Description |
|---|---|
http://localhost:8082/admin/master/console/ |
Realm admin (admin / admin) |
http://localhost:8082/realms/waggle-dev/.well-known/openid-configuration |
OIDC discovery endpoint |
Test users (both with password welkom123):
sam@example.comnoor@example.com
Backend .env setup (see also backend/.env.example):
WAGGLE_OIDC_ISSUER=http://localhost:8082/realms/waggle-dev
WAGGLE_OIDC_CLIENT_ID=waggle
WAGGLE_OIDC_CLIENT_SECRET=dev-not-secret
WAGGLE_PUBLIC_HOST=http://localhost:5173
WAGGLE_DEV_SYNTHETIC_AUTH=0
Verification once Keycloak has started:
curl -sf http://localhost:8082/realms/waggle-dev/.well-known/openid-configuration \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['issuer']); print(d['token_endpoint'])"
# Expected:
# http://localhost:8082/realms/waggle-dev
# http://localhost:8082/realms/waggle-dev/protocol/openid-connect/token
Stopping:
podman compose -f compose.dev.yaml down
Connecting a Mattermost token
Waggle needs a Mattermost token. There are two routes, depending on how your Mattermost instance is configured.
Route A - Personal access token (preferred)
Works when the Mattermost administrator has enabled personal access tokens.
- Log in to your Mattermost environment in the browser.
- Click your profile picture in the top left and choose Profile.
- Open the Security tab (in older versions: Account Settings → Security).
- Scroll to Personal Access Tokens and click Create.
- Give the token a name (e.g.
Waggle) and click Create. - Copy the Access Token that is shown (not the Token ID).
- Paste the token into Waggle (Instellingen → Mattermost) and save.
Route B - Browser session cookie
Works on instances where personal access tokens are disabled (among them the Digilab instance).
- Log in to Mattermost in your browser.
- Open the developer tools (F12 or Ctrl/⌘+Shift+I).
- Go to Application (Chrome/Edge) or Storage (Firefox) → Cookies and select your Mattermost domain.
- Find the
MMAUTHTOKENcookie and copy its value. - Paste that value into the Token field in Waggle.
The session token expires when you log out of Mattermost or when the session ends; you have to fetch it again after that.
Tokens are stored encrypted (Fernet, HKDF-derived key).
Tests
cd backend && uv run pytest -x -q # backend pytest (needs Postgres)
cd frontend && npm run typecheck # frontend typecheck
cd frontend && npm run build # frontend build (runs typecheck first)
npm run typecheck is vue-tsc. Do not substitute npx tsc --noEmit: plain
tsc does not look inside .vue templates, so it reports success on an invalid
component prop.
Through the justfile:
just test # backend pytest + frontend build
Building a preview
just preview # builds the frontend + starts the backend serving static on :8090
Requires WAGGLE_SECRET_KEY in .env or the environment.
Deploy
For the ZAD deployment (waggle.rijksapp.nl), see
docs/deployment.md. Platform findings and open questions
are in docs/zad-platform.md; decisions that are
expensive to reverse are recorded in docs/wdr/.
Licence
Open source under the EUPL-1.2. See publiccode.yml
for machine-readable metadata.
Developed and maintained by Robbert Bos (robbert.bos@rijksoverheid.nl) - see the disclaimer at the top of this file for context.