Self-hosted Kanban board for small groups. Blazor Server + Atmosphere (AT Protocol) OAuth + DID allowlist.
  • C# 38.7%
  • JavaScript 23%
  • HTML 21.3%
  • CSS 11.8%
  • Shell 2.9%
  • Other 2.2%
Find a file
Grandiras 646d19a4bd
All checks were successful
Publish / build-and-push (push) Successful in 38s
CI / build (push) Successful in 41s
Speak the v2 draft's one language on every page: one type scale, three button sizes, inspector dialogs (#261)
Every font size outside Home is on the draft's scale in px (no rem left), every button is one of three sizes (34px regular, .btn-sm 28px, .btn-lg 44px) set by a class in the markup instead of per-page copies, and What's new, Report a problem and the calendar's panels wear the inspector's dialog language with one shared CloseButton. The board's columns narrow beside a docked card, down to 240px, instead of running under it. Board settings says Delete, not Delete card. Net -390 lines of CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:21:36 +00:00
.claude/skills Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
.forgejo/workflows Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
deploy restore.sh opens an archive on a bare machine; release.sh stops handing deploy.sh a host (#230) 2026-09-23 15:18:19 +00:00
design Make search the v2 draft's palette: centred on desktop, full screen on a phone (#254) 2026-10-04 17:01:02 +00:00
docs Speak the v2 draft's one language on every page: one type scale, three button sizes, inspector dialogs (#261) 2026-10-05 19:21:36 +00:00
src/Ponente.Kanban Speak the v2 draft's one language on every page: one type scale, three button sizes, inspector dialogs (#261) 2026-10-05 19:21:36 +00:00
tests Speak the v2 draft's one language on every page: one type scale, three button sizes, inspector dialogs (#261) 2026-10-05 19:21:36 +00:00
tools Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
.dockerignore Milestone 1: skeleton 2026-04-09 22:22:06 +02:00
.gitignore Port the UI language v2 draft onto the canvas (#237) 2026-10-02 22:38:04 +00:00
AGENTS.md Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
CLAUDE.md Update ATProtoNet to 0.8.0 (#259) 2026-10-05 17:00:50 +00:00
docker-compose.test.yml Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
docker-compose.yml Debloat the repository: comments cut to the reasons, docs to their decisions (#229) 2026-09-23 15:16:27 +00:00
Dockerfile Claude: changes for issue #79 (#81) 2026-08-14 13:43:11 +00:00
Ponente.sln Claude: changes for issue #79 (#81) 2026-08-14 13:43:11 +00:00
README.md Update ATProtoNet to 0.8.0 (#259) 2026-10-05 17:00:50 +00:00

Ponente

A small self-hosted Kanban board for a private member group, built on the Atmosphere (AT Protocol) network. It holds any number of boards over one membership. Members sign in with an existing Atmosphere account, or with an account on the group's own bundled PDS (invite-code only). Everything the app stores is encrypted at rest.

Features

  • Kanban boards, each at its own link. Every member can make boards, and each board is open to every member or shared with the ones its admin picks. A board you were not given simply does not appear.
  • Atmosphere OAuth sign-in, gated by a DID allowlist.
  • Its own federated PDS (the official ghcr.io/bluesky-social/pds). Members without an Atmosphere account sign up with a single-use invite code, and their accounts federate with the network.
  • Access requests: a public /request form with a secret status link and an admin approval queue.
  • Encryption at rest: all user-supplied content is AES-256-GCM encrypted before it reaches Postgres, and OAuth tokens use ASP.NET Data Protection. PDS data lives in the PDS's own volume.
  • Cards with safe markdown, drag and drop, tags, assignees, comments and due dates.
  • Calendar (/calendar): a month grid and a per-tag timeline, with .ics export and subscription.
  • Live updates across tabs and members within about a second.
  • Installable as an app, in English and German.

Stack

.NET 10 Blazor Server · EF Core 10 + Postgres 17 · ATProto.NET 0.8.0 (OAuth with DPoP, PAR and PKCE; PDS administration) · Markdig · SortableJS (vendored)

Documentation

CLAUDE.md is the router and docs/orientation.md is the ten-minute tour. CLAUDE.md maps each area to the page in docs/ that explains why it is built the way it is. The deployment runbook is deploy/README.md.

First run

  1. Put your DID in Allowlist__Admins__0 in docker-compose.yml (look it up at web.plc.directory), or in appsettings.Development.json for host-side dev.
  2. Start the stack with podman compose up --build (or docker compose). The app runs on http://localhost:8090 and the PDS on http://localhost:8091. After code changes use up -d --build --force-recreate, because --build alone keeps the old container. podman compose down -v starts over.
  3. Sign in with your Bluesky username. The Members page in the account menu adds existing Atmosphere accounts and mints invite codes for the bundled PDS.

Development

podman run -d --name ponente-db \
  -e POSTGRES_DB=ponente -e POSTGRES_USER=ponente -e POSTGRES_PASSWORD=ponente \
  -p 5432:5432 docker.io/library/postgres:17-alpine
cd src/Ponente.Kanban && dotnet run        # http://127.0.0.1:5000

Migrations apply on startup. The PDS runs only in the compose stack (podman compose up pds).

dotnet build Ponente.sln && dotnet test Ponente.sln              # unit tests
podman compose -f docker-compose.yml -f docker-compose.test.yml up -d --build --force-recreate
cd tests/e2e && npx playwright test                               # e2e

Configuration

Env var Description
ConnectionStrings__KanbanDb Postgres connection string
Encryption__Key Required. Base64 32-byte key (openssl rand -base64 32). Losing it loses the data.
Allowlist__Admins__0 Bootstrap admin DID, seeded on first run
AtProto__BaseUrl Public base URL; the OAuth redirect URI derives from it (a loopback IP in dev)
AtProto__ProfileScope granular (default: write the profile record and upload an image), generic (transition:generic, for older account servers) or none (sign-in only). See docs/auth.md.
Pds__Url Internal PDS URL (e.g. http://pds:8080). Unset hides signup and invites.
Pds__PublicUrl / Pds__HandleDomain Public PDS URL and handle suffix (e.g. .ponente.app)
Pds__AdminPassword Shared secret for the PDS admin endpoints
AccessRequests__RateLimit Per-IP submissions per 15 minutes on /request (default 5)
Mail__SmtpUrl e.g. smtps://user%40host:pass@mail.example.com:465/ (percent-encode the @). Unset sends no mail.
Mail__FromAddress / Mail__FromName Sender. The address defaults to an address-shaped SMTP username, the name to Ponente.
Mail__AdminRecipients Recipients of new-request notifications, comma-separated
Feedback__Repository / Feedback__Token Forge repository (browser URL) and token for "Report a problem". Both are required, or the row is hidden.

Mail is optional and best-effort: new access requests go to admins, and an approval mails the requester their invite if they left an address. What is sent and why is in docs/access-requests.md.

The PDS takes the PDS_* variables in the compose files (see deploy/.env.example). Back up the PLC rotation key, because it controls account recovery. Set PDS_EMAIL_SMTP_URL and PDS_EMAIL_FROM_ADDRESS, or password resets silently fail. PDS_HANDLE_DOMAIN lets handles sit on the apex domain (alice.ponente.app) while the PDS runs on pds.ponente.app. Handle certificates come from Caddy's on-demand TLS, gated by an ask endpoint (docs/auth.md, deploy/README.md).

Releases and deployment

git tag vX.Y.Z && git push origin vX.Y.Z

That is the whole release. CI builds and pushes git.grandiras.net/grandiras/ponente-kanban:vX.Y.Z (and :latest), and a systemd timer on the server recreates the app within a minute. Pushes to main publish :main, which the dev stack (dev.ponente.app) follows. ./deploy/deploy.sh [vX.Y.Z] deploys from a workstation over the server's podman socket; pinning a version stops auto-updates, which is how rollbacks hold. Setup, prerequisites and rollbacks are in deploy/README.md and docs/deployment.md.

GET /healthz on both services returns 200 OK.