Files
paliad/docs/design-kanzlai-integration.md
m 460736ad1e refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.

Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
  i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
  global.css

Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
2026-04-30 16:46:31 +02:00

46 KiB
Raw Blame History

KanzlAI → Paliad Integration

Author: cronus (inventor) Date: 2026-04-16 Task: t-paliad-001 Revision: 2 (incorporates m's feedback via athena 2026-04-16: office-scoped visibility from day one; Mandate → Akten; Outlook as long-term risk; compliance handled out-of-band) Status: Design draft for review


Executive Summary

Recommendation: Full merge into a single Paliad codebase, single Go binary, single Supabase schema, single Dokploy deployment. Port KanzlAI's domain logic (deadline rules, calculator, holiday service, Akten/Fristen/Termine/notes models, CalDAV sync, AI extraction) into Paliad's existing Go backend. Rewrite KanzlAI's React+Next.js frontend in Paliad's Bun + TSX stack. Retire kanzlai.msbls.de to a 301 redirect once parity is reached.

Why this path. Two backends behind one domain doubles operational surface for zero user benefit. The two products share an audience (HLC patent practice), a stack philosophy (Go + Supabase), and an auth model (Supabase password). The only real cost is rewriting the React frontend — which is a smaller burden than it sounds, because KanzlAI's UI is mostly forms, lists, calendar grids, and tabbed details, all of which fit Paliad's server-rendered TSX + per-page client JS pattern. The merger from HL → HLC is the right moment to consolidate: one platform, one URL, one mental model. Knowledge tools and Aktenverwaltung belong together — a lawyer who looks up a UPC fee on Paliad should also see their next UPC deadline.


1. Integration strategy

Decision: Full merge into Paliad's stack

Four options were considered:

Option Backend Frontend UI Verdict
A. Run both apps behind unified domain 2 binaries (Go + Go) 2 stacks (Bun TSX + Next) 2 sidebars, reverse-proxy split Doubles ops; jarring UX seam between / and /app/*
B. Port frontend only, keep backends separate 2 binaries 1 stack (Bun TSX) 1 sidebar, 2 API clients Eliminates UI seam but keeps deploy/schema/auth split
C. Full merge — port KanzlAI into Paliad 1 binary 1 stack (Bun TSX) 1 sidebar Higher upfront cost, lowest ongoing cost
D. Incremental — start with deadlines, then add others Phased Phased Phased This is the implementation of C, not an alternative

Pick C, execute as D. Option C is the destination; the phased migration plan in §8 is the incremental path.

  • KanzlAI has unfixed critical security issues (AUDIT.md §1.1 tenant isolation bypass, §1.3 plaintext CalDAV creds, §1.4 no CORS, §1.6 race condition in HolidayService). Shipping it under the HLC name without fixing those is malpractice. Fixing them requires a full pass anyway — better to do the fixes inside Paliad than touch a deprecated stack.
  • React + Next.js + Tailwind v4 + react-query is too much machinery for Paliad's profile (small content + tools site). Maintaining two frontends is a tax we don't want.
  • KanzlAI has zero production users today (per audit). Now is the cheapest possible moment to consolidate.

Trade-off accepted

  • ~6 weeks of focused implementation (estimated 56h coder time, see §8) to retire ~16,500 lines of KanzlAI code and rebuild ~4,500 lines on Paliad's stack.
  • Lose React ecosystem (react-query, react-day-picker, etc.). Paliad's stack handles these patterns with HTML-first forms + small per-page TS bundles. Acceptable.

2. Auth & visibility

Decision: Office-scoped visibility from day one. Drop multi-tenancy infrastructure.

KanzlAI was built multi-tenant in case it became a SaaS product. That bet is dead — Paliad serves one firm. But "one firm" doesn't mean "every lawyer sees every Akte". Patent practice at HLC is office-anchored: a Munich associate works on Munich Akten; a partner may have visibility across offices; cross-office collaboration happens via explicit naming (collaborator lists), not via blanket access.

Strip the SaaS multi-tenancy plumbing (tenants, user_tenants, X-Tenant-ID headers, the bypass bug). Build office-scoped visibility natively in its place.

Concrete model

paliad.users table (extends auth.users)

paliad.users
  id              uuid PK / FK auth.users.id
  email           text (synced from auth.users; for display)
  display_name    text
  office          text NOT NULL CHECK (office IN
                    ('munich','duesseldorf','hamburg',
                     'amsterdam','london','paris','milan'))
  practice_group  text NULL  -- e.g. 'patents-litigation', 'patents-prosecution'
  role            text NOT NULL DEFAULT 'associate' CHECK (role IN
                    ('partner','associate','pa','admin'))
  created_at      timestamptz DEFAULT now()

office is mandatory; new users pick during first-login onboarding. practice_group is optional (used for filtering, not access control today).

Visibility on paliad.akten (and child rows)

Every Akte carries explicit visibility:

paliad.akten
  id                   uuid PK
  aktenzeichen         text NOT NULL  -- internal reference
  title                text NOT NULL
  status               text ...
  owning_office        text NOT NULL  -- the office the Akte "belongs to"
  collaborators        uuid[] DEFAULT '{}'  -- explicit user IDs with access
  firm_wide_visible    boolean NOT NULL DEFAULT false  -- partner-toggled override
  created_by           uuid FK paliad.users(id)
  created_at, updated_at
  ...

Visibility rule (the canonical predicate, used by RLS and by query helpers):

A user U can see Akte A iff:
  A.firm_wide_visible = true
  OR A.owning_office = U.office
  OR U.id = ANY(A.collaborators)
  OR U.role = 'admin'

RLS policies (enforced from day one)

A SQL function, then policies that use it:

CREATE OR REPLACE FUNCTION paliad.can_see_akte(akte_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $$
  SELECT EXISTS (
    SELECT 1
    FROM paliad.akten a
    JOIN paliad.users u ON u.id = auth.uid()
    WHERE a.id = akte_id
      AND (
        a.firm_wide_visible
        OR a.owning_office = u.office
        OR auth.uid() = ANY(a.collaborators)
        OR u.role = 'admin'
      )
  );
$$;

-- akten themselves
CREATE POLICY akten_select ON paliad.akten FOR SELECT
  USING (paliad.can_see_akte(id));
CREATE POLICY akten_insert ON paliad.akten FOR INSERT
  WITH CHECK (
    -- user can only create Akten in their own office
    -- (admins can create anywhere)
    owning_office = (SELECT office FROM paliad.users WHERE id = auth.uid())
    OR (SELECT role FROM paliad.users WHERE id = auth.uid()) = 'admin'
  );
CREATE POLICY akten_update ON paliad.akten FOR UPDATE
  USING (paliad.can_see_akte(id))
  WITH CHECK (paliad.can_see_akte(id));
CREATE POLICY akten_delete ON paliad.akten FOR DELETE
  USING (
    paliad.can_see_akte(id)
    AND (SELECT role FROM paliad.users WHERE id = auth.uid()) IN ('partner','admin')
  );

-- child rows (parteien, fristen, termine, dokumente, akten_events, notizen)
-- inherit visibility via akte_id
CREATE POLICY fristen_select ON paliad.fristen FOR SELECT
  USING (paliad.can_see_akte(akte_id));
-- ...analogous for the rest

Notes:

  • firm_wide_visible is the partner override for cross-office matters of firm-wide interest (e.g., a strategic UPC test case).
  • collaborators is the per-Akte explicit access list — how cross-office teams collaborate without flipping the firm-wide flag.
  • Only partners and admins can delete Akten; everyone with visibility can update.
  • Reference data (proceeding_types, deadline_rules, holidays) is global — RLS on but with USING (true) permissive policies. These tables hold no user data.
  • Knowledge content tables (link_suggestions, checklisten_feedback, gerichte_feedback) stay as-is — feedback is intentionally firm-wide.

Auth gate (registration)

Email domain whitelist, env-configured: [@hoganlovells.com, @hlc.com, @hlc.de]. Easy to extend if HLC picks a new TLD.

Onboarding flow (new in Phase D)

After first sign-in: prompt for office (required) + practice group (optional) + role (defaults to associate; partners self-identify, admins set by SQL initially). Without this row in paliad.users, the user can't see any Akten — that's the safe default.

Backend implications

  • All Akten queries route through a service.ListVisibleForUser(ctx, userID) helper that enforces the predicate at the application layer too (defense in depth — RLS is the floor, not the ceiling).
  • "All Akten in the firm" view exists for admins/partners as a feature, not as the default.
  • The Dashboard (Phase G) shows visible Akten only.

Post-merger email transition

  • Phase 1 (now → merger): whitelist [@hoganlovells.com, @hlc.com, @hlc.de]. Any new HLC subdomain TLD added to the env config slice.
  • Phase 2 (post-merger): existing @hoganlovells.com Supabase identities continue to work (Supabase keys on UUID, not email). New users register under @hlc.*. No data migration needed.
  • Phase 3 (cleanup, +1 year): optional script to update display emails in paliad.users if HLC actually retires the old domain.

When the model needs to extend

  • Practice-group scoping (e.g., "Patents Litigation can't see Patents Prosecution Akten"): add visible_to_groups text[] column on paliad.akten, extend the RLS predicate. Don't build now — wait until a partner asks.
  • External counsel access (e.g., bringing in an Italian boutique on a Milan Akte): add a separate external_collaborators table with email + scoped RLS. Don't build now.
  • Read-only Akten archive after closure: add is_archived flag, deny mutations via RLS. Cheap follow-on.

3. Database / schema

Decision: Single paliad schema in the youpc Supabase instance. Migrate kanzlai tables in.

KanzlAI's schema lives in kanzlai.* on the youpc Supabase instance. Paliad currently has 3 feedback tables (link_suggestions, checklisten_feedback, gerichte_feedback) that look like they're in the public schema (no schema prefix in docs/migrations/00*.sql).

Plan:

  1. Create paliad schema in the same youpc Supabase instance (no new project).
  2. Move Paliad's 3 existing feedback tables into paliad.* (CREATE in new schema, COPY data, drop old).
  3. Port KanzlAI's tables into paliad.*, dropping multi-tenancy fields and adding visibility columns:
    • casespaliad.akten (German throughout: table name, Go struct Akte, URL /akten, UI label "Akten")
    • partiespaliad.parteien
    • deadlinespaliad.fristen
    • appointmentspaliad.termine
    • documentspaliad.dokumente
    • case_eventspaliad.akten_events
    • notespaliad.notizen
    • All drop tenant_id, all add akte_id FK to paliad.akten, all gain visibility via paliad.can_see_akte(akte_id) in RLS.
    • paliad.akten itself adds owning_office, collaborators uuid[], firm_wide_visible boolean per §2.
    • Reference tables: proceeding_types, deadline_rules, holidayspaliad.*. RLS-on but permissive (no user data).
    • New: paliad.users per §2.
  4. RLS enabled on all paliad tables with real visibility policies per §2 (not just permissive-authenticated).
  5. Retire kanzlai.* schema once cutover is verified (DROP SCHEMA kanzlai CASCADE in Phase J).

German vs. English naming

Going fully German end-to-end (table, struct, URL, UI). Reasons:

  • Paliad already uses German nouns for routes (/fristen, /termine, /glossar, /checklisten). Mixing German routes with English internals creates lifelong cognitive translation overhead — KanzlAI showed this with cases (English) ↔ "Akten" (German UI).
  • The audience reads German legal terminology natively.
  • Go struct names allow Unicode-clean ASCII (Akte, Frist, Termin) — no friction.
  • Database identifiers in lowercase German plurals (akten, fristen, parteien) read fine to anyone who's worked with German data.

Concrete naming convention:

Concept DB table Go struct Go service URL German UI
Matter paliad.akten Akte AkteService /akten "Akte" / "Akten"
Party paliad.parteien Partei ParteienService (sub of Akte) "Partei" / "Parteien"
Deadline paliad.fristen Frist FristService /fristen "Frist" / "Fristen"
Appointment paliad.termine Termin TerminService /termine "Termin" / "Termine"
Document paliad.dokumente Dokument DokumentService (sub of Akte) "Dokument"
Audit event paliad.akten_events AkteEvent (in AkteService) n/a "Verlauf"
Note paliad.notizen Notiz NotizenService (cross-cutting) "Notiz" / "Notizen"
User paliad.users User UserService n/a n/a

User stays English (it's a Supabase concept, not a legal one).

Reuse vs. port

  • Reuse the data: KanzlAI's deadline_rules table has 32 UPC + 4 ZPO rules (more than Paliad's hardcoded internal/calc/deadline_rules.go). Port the data, drop Paliad's hardcoded rules.
  • Reuse the holiday data: KanzlAI's holidays table has DE federal + UPC judicial vacations 2026. Port and extend.
  • Don't reuse: KanzlAI's tenants, user_tenants, billing_rates, invoices, time_entries (the latter three are aspirational — handler stubs exist but no real implementation). Drop entirely.

Migration tooling

Paliad currently has SQL files in docs/migrations/ with no runner. Pick a tool now while the schema is small:

  • Choice: golang-migrate/migrate as a sidecar in the Docker entrypoint. Migrations live in migrations/ (top-level, not docs/migrations/ — that path is wrong; move them). Numbered 00N_description.up.sql + 00N_description.down.sql. Applied at server startup before the HTTP listener binds.
  • Why golang-migrate over Atlas/Goose: simplest, single binary, well-known, plays nicely with Supabase Postgres.

Schema collisions

None. Paliad's existing 3 feedback tables don't overlap with anything in KanzlAI. The risk vector was cases vs. akten naming — resolved by going fully German.


4. Feature prioritization

KanzlAI shipped a lot in one session; not all of it was production-quality. Cherry-pick.

Ports (P0 — first cutover)

  • Deadline rules + holidays + calculator (KanzlAI: holidays.go, deadline_calculator.go, deadline_rule_service.go). Replaces Paliad's internal/calc/deadline_rules.go hardcoded data.
  • Akten CRUD + Parteien (KanzlAI: case_service.go, parties.go). Foundation for everything below. Includes the office-scoped visibility logic from §2.
  • Frist management UI (KanzlAI: DeadlineList, DeadlineCalendarView, traffic light cards). Extends current Fristenrechner with persistence.

Ports (P1 — fast follow)

  • Termine + CalDAV sync (KanzlAI: appointment_service.go, caldav_service.go). CalDAV is a real differentiator. Mandatory fix during port: encrypt CalDAV credentials at rest (audit §1.3).
  • Dashboard (KanzlAI: dashboard_service.go, dashboard widgets). Replaces Paliad's logged-in landing.

Ports (P2 — once foundation is stable)

  • AI Frist extraction (KanzlAI: ai_service.go, document upload). Dependency on Anthropic key in Dokploy.
  • Notizen (polymorphic notes table, KanzlAI Phase A backend). Cross-cutting; useful but not blocking.

Drop entirely

  • Billing / RVG (billing_rates.go, invoices.go, time_entries.go) — KanzlAI has handler stubs, no real impl. The audit (§8.2) calls out beA + RVG as table-stakes for competing with RA-MICRO. We're not competing with RA-MICRO. HLC has its own firm-wide billing. Don't pretend.
  • reports.go, notifications.go, templates.go, tenant_handler.go, case_assignments.go — all aspirational stubs in KanzlAI. Drop.
  • Multi-tenancy infrastructure (tenants, user_tenants, X-Tenant-ID middleware) — see §2; replaced by office-scoped visibility.
  • Reports/audit endpoints beyond the basic akten_events audit log.

Knowledge platform features that survive

Paliad's existing tools (Kostenrechner, Glossar, Gebührentabellen, Checklisten, Gerichte, Links, Downloads) all stay as-is. They sit alongside the new Aktenverwaltung features in the same sidebar.

Knowledge platform features that change

  • Fristenrechner evolves: same calculator UI, but now backed by the same DB as Frist management. Adds a "Zur Akte speichern" button that creates persistent Fristen on a chosen Akte.
  • Roadmap §2.3 UPC Rechtsprechungsübersicht — dropped entirely (per task brief). Replaced by a curated link in the Link Hub pointing to youpc.org.

5. Roadmap update

Changes to docs/feature-roadmap.md

Rewrite the "What patholo Is NOT" section (lines 527534). New text:

### What Paliad Is

Paliad is the all-in-one platform for HLC patent practice:

- **Knowledge platform** — curated content, practical tools, quick reference
  (Glossar, Gebührentabellen, Checklisten, Gerichtsverzeichnis, Leitfäden, Links).
- **Aktenverwaltung** — Akten, Fristen, Termine, Parteien, Dokumente, Notizen,
  Verlauf (audit trail). Office-scoped visibility with explicit collaborator
  lists for cross-office teams. Personal calendar sync via CalDAV. AI-assisted
  Frist extraction from uploaded court documents.

What Paliad is *not*:

- Not a billing tool — HLC has firm-wide billing infrastructure.
- Not a beA gateway — out of scope; lawyers use existing beA software.
- Not a document management system — SharePoint/netDocuments stay in their lane.
- Not a CMS — content lives in git, not a database with a CMS UI.

Drop §2.3 UPC Rechtsprechungsübersicht entirely. Replace with one Link Hub entry under "Recherche" pointing to youpc.org/upc/rechtsprechung (or wherever the curated overview lives). Save 14h of work; defer to youpc.org which already has the data.

Update §4.2 Fristenkalender. Currently P3, "8h, High impact". Promote to P0 — it's the entry point to the Aktenverwaltung features, not an optional add-on. Replace with: "Fristenkalender → see §8 Phase E (Frist management UI), Phase F (Termine + CalDAV sync)."

Add new sections (as a new top-level "Phase 0: Aktenverwaltung Foundation"):

### 0.1 Akten (Matter Management) — office-scoped
### 0.2 Fristen (Persistent Deadline Management)
### 0.3 Termine + CalDAV Sync
### 0.4 Dashboard (Logged-in Landing)
### 0.5 AI-assisted Frist-Extraktion
### 0.6 Notizen

Each section should reference the per-phase task in §8 of this design doc rather than duplicating estimates.

Update prioritized backlog table (lines 463483): insert the 6 Phase 0 features at the top with P0 priority. Demote 1.5 (Kostenrechner enhancements) to P1 — Aktenverwaltung beats Kostenrechner polish.

Update Architecture Notes "Data Strategy" (line 489). Currently says "Most Phase 1 and 2 features use static JSON data." Add: "Phase 0 features (Aktenverwaltung) use Supabase tables with office-scoped RLS. The split is: reference data + curated content = git/JSON; user-generated data = DB."


6. UI integration

Decision: One sidebar, six grouped sections

Current Paliad sidebar is flat. KanzlAI has its own React sidebar with cases-first navigation. Merge into a single grouped sidebar:

PALIAD (logo, lime green)

— ÜBERSICHT —
  Dashboard

— ARBEIT —
  Akten
  Fristen
  Termine

— WERKZEUGE —
  Kostenrechner
  Fristenrechner       ← stays as a quick-calc tool, distinct from /fristen
  Gebührentabellen

— WISSEN —
  Glossar
  Checklisten
  Gerichtsverzeichnis
  Leitfäden            (future, per current roadmap §2.1)

— RESSOURCEN —
  Downloads
  Nützliche Links

— FOOTER —
  DE / EN
  user@example.com  →  Logout

Rationale:

  • "Übersicht" (Dashboard) is the new home for logged-in users. The current / marketing-ish landing becomes the unauthenticated welcome screen.
  • "Arbeit" = the user's active workload. This is what KanzlAI provided; it's now the top of the new value stack.
  • "Werkzeuge" = stateless calculators / lookups. Including Fristenrechner here as a quick scratch tool, separate from /fristen which is the persistent Fristen list. (Yes, two deadline-related routes — they serve different jobs.)
  • "Wissen" = curated content.
  • "Ressourcen" = files + external links.

Navigation in detail

  • / — unauthenticated landing OR redirects authenticated users to /dashboard.
  • /dashboard — Dashboard (KanzlAI port).
  • /akten, /akten/neu, /akten/[id], /akten/[id]/{verlauf|fristen|termine|dokumente|parteien|notizen} — Akten list + detail with sub-pages.
  • /fristen, /fristen/neu, /fristen/[id], /fristen/kalender — persistent Fristen.
  • /termine, /termine/neu, /termine/[id], /termine/kalender — Termine.
  • /einstellungen/caldav — per-user CalDAV configuration.
  • /onboarding — first-login office/practice-group/role setup (Phase D).
  • /tools/fristenrechner — stays at current path (don't break links). Adds "Zur Akte speichern" button.
  • All other knowledge routes unchanged.

Visual language

  • Lime accent (#c6f41c) stays — it's the brand. Used for active nav, primary CTAs, traffic-light "go", new-since-last-visit badges.
  • Traffic light colors for Fristen: red #ef4444 (überfällig), amber #f59e0b (diese Woche), green #22c55e (OK). KanzlAI's choices, keep them.
  • Type colors for Termine: hearing=blue, meeting=violet, consultation=emerald, deadline_hearing=amber. KanzlAI's choices, keep them.
  • Akten list: a small office badge per row (Munich, Düsseldorf, etc.) so cross-office collaborators can scan visibility origin at a glance.
  • Sidebar: existing Paliad styling, with section headers (small caps, low contrast) added.

Frontend rewrite scope

Pages to build in Bun TSX:

  • dashboard.tsx (1 page)
  • akten.tsx, akten-detail.tsx, akten-neu.tsx (3 pages, plus sub-routes via Go mux)
  • fristen.tsx, fristen-detail.tsx, fristen-neu.tsx, fristen-kalender.tsx (4 pages)
  • termine.tsx, termine-detail.tsx, termine-neu.tsx, termine-kalender.tsx (4 pages)
  • einstellungen-caldav.tsx (1 page)
  • onboarding.tsx (1 page — first-login form)

Per-page client-side TS bundles in src/client/ for interactive bits (calendar nav, mark complete, collaborator-picker autocomplete, modal forms). HTML-first: forms POST and reload by default; JS enhances where it improves UX (calendar pagination, optimistic mark-complete).

No react-query, no Tailwind v4. Use existing global.css patterns.


7. Deployment

Decision: Single Dokploy compose, retire kanzlai.msbls.de

  • Production target: existing Paliad compose Zx147ycurfYagKRl_Zzyo. No new infra.
  • Domains on the compose:
    • paliad.de — primary
    • patholo.de — legacy redirect (already configured)
    • patholo.msbls.de — internal preview (already configured)
    • Add: kanzlai.msbls.de → 301 redirect to paliad.de (Phase J)
  • KanzlAI Dokploy app — stop and delete after Phase J cutover. Free up the resources.
  • KanzlAI Gitea repo (m/KanzlAI-mGMT) — archive (set read-only) after cutover. Keep for git history reference.

Build and deploy

  • Existing flow stays: push to main on m/paliad → Gitea webhook → Dokploy auto-deploy.
  • Dockerfile changes: add migration step to entrypoint (run migrate up against DATABASE_URL before starting the HTTP server).
  • New env vars in Dokploy:
    • DATABASE_URL (youpc Supabase Postgres conn string)
    • SUPABASE_SERVICE_KEY (for storage access during AI extraction)
    • ANTHROPIC_API_KEY (for AI extraction; AI features stay disabled if unset)
    • CALDAV_ENCRYPTION_KEY (32-byte AES key for credential-at-rest encryption)
  • Existing env vars unchanged: SUPABASE_URL, SUPABASE_ANON_KEY, GITEA_TOKEN, PORT.

Cutover sequence

Single deployment (no parallel run). Because KanzlAI has zero production users, the cutover is just:

  1. Apply migrations (Phase A).
  2. Ship phases BH to paliad.de.
  3. Verify functionality.
  4. Flip kanzlai.msbls.de to 301-redirect to paliad.de (Phase J).
  5. Stop KanzlAI Dokploy app.

If KanzlAI ever gets users before cutover, switch to a parallel-run-with-data-migration model. Right now, no users = no data migration.


8. Migration plan (phased task breakdown)

Each phase is sized for one focused coder session with clear acceptance criteria. Effort estimates assume Sonnet implementation with Opus design review where flagged.

Phase A — Database foundation, visibility model & migration tooling (~8h)

Scope:

  • Create paliad schema in youpc Supabase instance.
  • Add golang-migrate to the Docker entrypoint.
  • Move existing Paliad migrations (docs/migrations/00{1,2,3}.sql) → migrations/00{1,2,3}_*.up.sql + .down.sql. Re-namespace tables into paliad.*.
  • Create paliad.users table with office, practice_group, role columns per §2.
  • Port KanzlAI tables → paliad.* migrations with German names: proceeding_types, deadline_rules, holidays, akten (renamed from cases, with owning_office + collaborators uuid[] + firm_wide_visible columns), parteien, fristen, termine, dokumente, akten_events, notizen. Drop tenant_id everywhere; add created_by FK to paliad.users.id.
  • Create paliad.can_see_akte(akte_id uuid) SQL function per §2.
  • RLS policies per §2: akten_select/insert/update/delete using the visibility predicate; child-row policies (fristen, termine, parteien, dokumente, akten_events, notizen) using paliad.can_see_akte(akte_id). Reference tables RLS-on but USING (true).
  • Indexes per audit §3.3 + visibility queries: (owning_office) on akten, (status, due_date) on fristen, (start_at) on termine, (akte_id, created_at) on akten_events, (status) on akten, GIN index on collaborators for = ANY lookups.
  • Seed: 6 proceeding types, 32 UPC + 4 ZPO rules, 68 holidays (port directly from KanzlAI seed).
  • Seed paliad.users with cronus's test user as admin for local dev (real users register via the app).

Acceptance:

  • \dt paliad.* shows ≥12 tables (including users).
  • RLS enabled on all (verified via pg_class.relrowsecurity).
  • Visibility predicate verified with 3 test users in 2 offices: each sees only their own office's Akten + any with their UUID in collaborators + any with firm_wide_visible=true.
  • Migrations run cleanly up + down.
  • Seed data present.
  • Server starts after migrate up runs in entrypoint.

Owner suggestion: coder. Opus review the RLS policy + visibility predicate before merge — this is the security-critical layer.


Phase B — Backend foundation (sqlx, services, no UI yet) (~8h)

Scope:

  • Add internal/db/ package (sqlx connection pool with table references qualified to paliad.* — don't rely on session-level search_path per audit §2.8).
  • Port internal/services/holidays.go from KanzlAI (with sync.RWMutex fix per audit §1.6).
  • Port internal/services/deadline_calculator.go (UUID-based; works from DB rules).
  • Port internal/services/deadline_rule_service.go (read from DB).
  • Add internal/services/user_service.go — bootstrap paliad.users row from auth.users, get current user from request context, list users.
  • Add internal/services/akte_service.go (formerly case_service.go). Visibility-aware: ListVisibleForUser(ctx, userID) enforces the §2 predicate at app layer (defense in depth alongside RLS). Create/Update/Delete enforce role + office checks.
  • Port internal/services/parteien_service.go.
  • Tests: deadline calculator round-trip, holiday adjustment, Akte visibility (3-user 2-office scenario), Akte CRUD happy-path with role enforcement.

Acceptance:

  • GET /api/deadline-rules returns rules from DB.
  • POST /api/fristen/calculate works with DB rules (proves Phase A schema ↔ Phase B services wiring).
  • GET/POST /api/akten works (no UI yet — verified via curl with 3 test users).
  • Visibility verified end-to-end: User A (Munich associate) cannot see User B (Düsseldorf associate)'s Akte unless added as collaborator or firm_wide_visible=true.
  • All ported tests pass.
  • No regressions in existing Paliad endpoints.

Owner suggestion: coder. Opus review the visibility enforcement code paths.


Phase C — Migrate existing Fristenrechner to DB-backed rules (~3h)

Scope:

  • Delete internal/calc/deadline_rules.go hardcoded data.
  • Update internal/handlers/fristenrechner.go to call DB-backed services from Phase B.
  • Verify existing /tools/fristenrechner UI is byte-identical (no UX regression).
  • Update internal/calc/deadlines.go: delete logic that's now in services/deadline_calculator.go; thin shim if needed for the existing JSON shape.

Acceptance:

  • /tools/fristenrechner page identical UX, same dropdowns, same calc results.
  • No hardcoded rules in internal/calc/.
  • Existing fristenrechner tests pass against new code path.

Owner suggestion: coder.


Phase D — Akten CRUD + onboarding + collaborator UI (~8h)

Scope:

  • Backend: handlers for GET/POST/PATCH/DELETE /api/akten, GET/POST /api/akten/{id}/parteien, GET /api/users (for collaborator-picker autocomplete, returns minimal user list).
  • Backend: POST /api/onboarding — accepts office/practice_group/role for the current auth.uid(), creates the paliad.users row.
  • Backend: PATCH /api/akten/{id}/collaborators (add/remove), PATCH /api/akten/{id}/firm-wide-visible (partner-only — checked via role).
  • Frontend (TSX):
    • onboarding.tsx — first-login form. Middleware-detected: if paliad.users row missing, redirect here on any protected route.
    • akten.tsx — list scoped to visible Akten. Office badge per row. Filter by office (own / firm-wide / all-visible).
    • akten-neu.tsx — form. owning_office defaults to user's own office; admin can pick any.
    • akten-detail.tsx — header + tabs. Header shows office badge + collaborator avatars + firm-wide-visible flag. Collaborator-picker modal for adding/removing. Partner-only "Firm-wide visible" toggle.
  • Sidebar: add "Arbeit" group with Akten as the only entry initially.
  • Per-page client TS for inline edit, delete confirm, collaborator autocomplete.

Acceptance:

  • New user signs up → redirected to /onboarding → completes form → redirected to /dashboard.
  • Lawyer creates Akte → automatically owns it under their office.
  • Lawyer in another office cannot see the Akte unless added as collaborator.
  • Partner toggles firm_wide_visible → all users now see it.
  • Audit trail: each mutation creates an akten_events row.
  • Empty states are friendly (German Umlaute correct per audit §2.10).

Owner suggestion: coder. Opus review the visibility-related UI flows (collaborator picker, firm-wide toggle).


Phase E — Persistent Frist management UI (~6h)

Scope:

  • Backend: handlers for GET /api/fristen (all visible), GET/POST /api/akten/{id}/fristen, PATCH /api/fristen/{id}, PATCH /api/fristen/{id}/complete, DELETE /api/fristen/{id}. All visibility-enforced via the parent Akte.
  • New TSX pages: fristen.tsx (list with status filter — überfällig/diese_woche/alle), fristen-neu.tsx, fristen-detail.tsx, fristen-kalender.tsx (month grid).
  • Traffic light cards on fristen.tsx + on akten-detail.tsx (Fristen tab).
  • "Zur Akte speichern" button on existing Fristenrechner: select target Akte from dropdown of visible Akten, persists calculated Fristen as draft.

Acceptance:

  • Lawyer creates Akte, adds Fristen manually OR via Fristenrechner "Zur Akte speichern".
  • List filterable by status. Calendar view navigable by month.
  • Mark-complete works, persists, updates traffic light counts.
  • Lawyer cannot see Fristen on Akten they don't have visibility to.

Owner suggestion: coder.


Phase F — Termine + CalDAV sync (~8h)

Scope:

  • Backend: termin_service.go, handlers for CRUD. Visibility via parent Akte.
  • Backend: caldav_service.go ported from KanzlAI with credential encryption fix (audit §1.3): use CALDAV_ENCRYPTION_KEY env var with AES-GCM at rest. Never return password in API responses.
  • New TSX pages: termine.tsx, termine-neu.tsx, termine-detail.tsx, termine-kalender.tsx, einstellungen-caldav.tsx.
  • Per-user CalDAV config (one entry per auth.uid(), stored in new paliad.user_caldav_config table).
  • Background sync goroutine per user with active config (pulled at server startup, refreshed when config changes).
  • Sync scope: only sync Termine on Akten the user can see (visibility predicate applies to outbound sync too).

Acceptance:

  • User configures CalDAV in settings, password is encrypted in DB.
  • User creates a Termin, it appears in their external calendar within 60s.
  • External calendar event edited → reflected in Paliad on next sync cycle.
  • Deleting CalDAV config purges credentials.
  • User cannot leak Termine to their personal calendar from Akten they don't have visibility to.

Owner suggestion: coder, Opus design review of the encryption scheme + sync conflict resolution + visibility-aware sync scope.


Phase G — Dashboard (~4h)

Scope:

  • Backend: dashboard_service.go ported and simplified (no tenant; uses visibility predicate). Returns frist_summary, akten_summary, upcoming_fristen (7d), upcoming_termine (7d), recent_activity — all scoped to what the user can see.
  • New TSX page: dashboard.tsx — server-rendered initial paint (no client-side fetch + skeleton, per audit §2.3 critique).
  • Update / route: redirect authenticated users to /dashboard; serve current marketing-ish landing only for unauthenticated visitors (or remove entirely if HLC sees no value in the unauthenticated landing).
  • Update sidebar "Übersicht → Dashboard" entry.

Acceptance:

  • Authenticated user lands on /dashboard with zero client-side waterfall.
  • Traffic lights link to /fristen?status=....
  • Upcoming items link to detail pages.
  • Recent activity shows last 10 akten_events with author + timestamp, scoped to visible Akten.

Owner suggestion: coder.


Phase H — AI Frist-Extraktion (~4h)

Scope:

  • Backend: ai_service.go ported from KanzlAI. Anthropic SDK call with PDF document blocks + tool-forced structured output.
  • Backend: document upload → Supabase Storage, then AI extraction triggered. Visibility check on parent Akte before processing.
  • Frontend: document upload component on akten-detail.tsx "Dokumente" tab.
  • Extraction result: review screen with confidence scores + source quotes; user confirms which Fristen to persist.
  • Feature flag: AI tab/buttons hidden if ANTHROPIC_API_KEY is unset.
  • Per-user rate limit on AI endpoint (fixes audit §1.7 by keying off auth.uid(), not spoofable headers).

Acceptance:

  • Upload a UPC Statement of Defence PDF → AI extracts Fristen with rule references → user confirms → persisted as draft Fristen on the Akte.
  • No AI calls on uploads to other Akten (visibility-checked).
  • Rate limit caps abuse.

Owner suggestion: coder.


Phase I — Notizen (polymorphic) (~4h)

Scope:

  • Backend: notizen table migration (polymorphic FK with CHECK constraint per KanzlAI Phase A pattern), notiz_service.go, handlers. Visibility inherited from parent Akte (when notiz attaches to Akte/Frist/Termin/AkteEvent).
  • Frontend: shared NotizenList TSX component, embedded on Akte / Frist / Termin / AkteEvent detail pages.

Acceptance:

  • User adds Notiz to an Akte → visible on Akte detail.
  • User adds Notiz to a Frist → visible on Frist detail.
  • Edit/delete works.
  • Visibility inherited correctly.

Owner suggestion: coder.


Phase J — Roadmap rewrite + KanzlAI retirement (~3h)

Scope:

  • Rewrite docs/feature-roadmap.md per §5 of this doc.
  • Add kanzlai.msbls.de domain to Paliad Dokploy compose with 301 redirect rule (Traefik label or middleware).
  • Stop and delete KanzlAI Dokploy app.
  • Archive m/KanzlAI-mGMT Gitea repo (set to read-only / archived).
  • Update mai.projects: optionally merge kanzlai project into paliad (decide: keep history separate or unify).
  • Memory: write episode in paliad group documenting the consolidation, supersede KanzlAI episodes.
  • Drop kanzlai schema from Supabase: DROP SCHEMA kanzlai CASCADE after final verification that no Paliad code references it.

Acceptance:

  • kanzlai.msbls.de redirects to paliad.de with 301.
  • KanzlAI Dokploy app gone.
  • KanzlAI repo archived.
  • Roadmap doc reflects the new strategy.
  • kanzlai schema dropped.

Owner suggestion: head + cronus together (mostly ops, a bit of writing).


Total effort

~56 hours of focused implementation across 10 phases (revised up from 52h: Phase A +2h for visibility model, Phase D +2h for onboarding + collaborator UI), plus this design review (~3h) = ~59h total. At 12 phases per day with one coder, this completes in 23 working weeks.


9. Risks & unknowns

Risks

  1. KanzlAI's audit issues must not be ported as-is. The audit identified 7 critical and 13 important issues. Each port phase is responsible for fixing the issues relevant to its code: Phase A enforces real (not permissive) RLS policies (audit §2.13), Phase B fixes HolidayService race + search_path (§1.6, §2.8) and the tenant-isolation pattern (§1.1, replaced by visibility predicate), Phase F fixes CalDAV plaintext (§1.3), Phase H fixes the rate-limiter spoofability (§1.7). Every backend phase implements proper error handling (§1.5) + length limits + pagination (§2.1, §2.2). Track these in a checklist per phase.

  2. React → Bun TSX rewrite friction. KanzlAI's frontend uses react-query, react-day-picker, modal libs, complex client state. Paliad's stack uses server-rendered TSX + small per-page client TS. Some patterns translate awkwardly. Mitigation: HTML-first design, JS only where it materially improves UX. If a feature requires a heavy interactive component (e.g., date picker), prefer native <input type="date"> over importing a library.

  3. Visibility model is the security-critical layer. Office-scoped RLS with collaborator override is more complex than either "everyone sees everything" or pure tenant-id. Bugs here leak Akten across offices. Mitigation: Phase A and Phase B both have Opus design reviews on the visibility predicate; Phase B includes a 3-user 2-office integration test as an acceptance gate; the predicate lives in a single SQL function (paliad.can_see_akte) reused by every RLS policy and mirrored at the app layer for defense in depth. Add a pen-test pass (or at minimum a security-review skill run) before Phase J.

  4. Email domain transition risk. Whitelist [@hoganlovells.com, @hlc.com, @hlc.de]. Risk: HLC picks a TLD we didn't anticipate (@hlc.law?). Mitigation: keep the whitelist in env config, not hardcoded. Trivial to update.

  5. CalDAV encryption key rotation. If CALDAV_ENCRYPTION_KEY is ever lost or rotated, all stored credentials become unreadable. Mitigation: document the recovery posture (users re-enter CalDAV password from their existing software), commit to never rotating without a re-encrypt migration.

  6. KanzlAI-derived German Umlaut typos in audit §2.10. Don't carry them forward. Style check during each frontend port phase.

  7. Outlook / Exchange CalDAV is not where HLC lawyers live. HLC primarily uses Outlook + Exchange; Exchange's CalDAV support is limited or off by default. Phase F ships with CalDAV only (verified against dav.msbls.de and Apple iCloud as KanzlAI did). Long-term plan: add an EWS / Microsoft Graph API backend behind the same sync abstraction. That's a follow-on phase (call it Phase K), not blocking the v1 cutover. For HLC users on Outlook, Phase F will work for manual iCal export; full bidirectional sync waits for Phase K.

  8. No load testing on KanzlAI's calc-heavy paths. AI extraction in particular can be slow + expensive. Phase H adds per-user rate limits before going live (per-user via auth.uid(), not header).

Unknowns

  • Practice-group scoping intent. §2 makes office the access boundary and treats practice_group as filter-only metadata. Confirm with a HLC partner: are there matters where Patents Litigation must be walled off from Patents Prosecution? If yes, schema is ready (extend the predicate); if no, status quo.
  • AI extraction quality on real HLC documents. KanzlAI's test fixtures were synthetic. Real UPC Statements of Defence are messier. Phase H should include a manual test pass on 35 real (anonymized) HLC documents before declaring done.
  • Outlook integration urgency. Phase K (EWS / Graph) could be ahead of Phase J if a partner says CalDAV-only is a non-starter at HLC. Decide at end of Phase F based on internal feedback.
  • Naming check with a HLC partner. "Akten" / "Aktenverwaltung" is the German legal-practice norm — confirm one final time before the strings are baked in across the UI.

10. Open question for the head

Once this design is approved, who implements?

  • Option 1: I (cronus) implement Phases AJ as coder. I have the deepest context from this design work and the source repos. Single context, no handoff loss. Likely faster overall.
  • Option 2: Hand off to a separate coder worker (e.g., pike, knuth, ritchie) per phase. Better parallelism if multiple phases can run in parallel. But Phases A → B → C → D have hard dependencies; only F, G, H, I can run in any order after E.
  • Option 3: Hybrid — I implement Phases AC (foundation, including the security-critical visibility model), hand off DI to coder workers in parallel where possible, take J myself for the wrap-up.

My recommendation: Option 3. Foundation needs design context (cronus is the carrier), and the visibility model in §2 is too security-critical to hand off cold. Phases DI are mechanical ports of well-defined services; coder workers can run them with the design doc + KanzlAI source as the brief. Phase J is the easy victory lap.


11. Post-Integration Status (added 2026-04-17)

Recorded after Phase J documentation pass on branch mai/ritchie/phase-j-roadmap-rewrite.

Shipped phases

Phase Scope Status Merge
A Database foundation, visibility model, migration tooling Shipped 1b2ef28 (2026-04-16)
B sqlx pool, services, Akten/Frist endpoints Shipped bcc4939 (2026-04-16)
C Fristenrechner → DB-backed Shipped d1909c7 (2026-04-16)
D Akten CRUD + onboarding + collaborator UI Shipped 4296da5 (2026-04-16)
E Persistent Frist management UI Shipped 316dc9f (2026-04-16)
F Termine + CalDAV sync (AES-GCM at rest) Shipped b56ef66 (2026-04-17)
G Dashboard (server-rendered) Shipped b79ef25 (2026-04-16)
H AI Frist-Extraktion Deferred branch mai/ritchie/phase-h-ai-deadline — not merged
I Notizen (polymorphic service + UI) Pending Schema only (migrations 005/007); service and UI not started
J Roadmap rewrite + KanzlAI retirement 🟡 Docs only — infra pending see below

Phase H — deferred (not cancelled)

Decision by m on 2026-04-16: "We don't want Anthropic API. We put this off for a while." The work on branch mai/ritchie/phase-h-ai-deadline (commit f539102) covers the extraction path end-to-end, but will not be merged until the Anthropic API decision is revisited. The Dokumente tab on Akten detail stays as a "Kommt bald" placeholder. No ANTHROPIC_API_KEY on Dokploy.

Document upload + Supabase Storage alone (without AI) remains an open question — potentially worth shipping as a standalone Dokumente feature even with AI deferred.

Phase I — pending

paliad.notizen table with polymorphic FK + CHECK constraint and RLS is already in place (migrations 005 and 007). The service (notiz_service.go), handlers, and the shared NotizenList TSX component are not yet built. Picks up as ~4h of focused work when the cross-cutting notes become the next friction point.

Phase J — partial (documentation done; infra retirement pending)

Done in this Phase J pass (2026-04-17, branch mai/ritchie/phase-j-roadmap-rewrite):

  • docs/feature-roadmap.md rewritten per §5 of this doc: all-in-one positioning, Phase 0 Aktenverwaltung section with completed items, "What Paliad Is" replaces "What patholo Is NOT", dropped §2.3 UPC Rechtsprechung (youpc.org link covers it), updated prioritized backlog with done markers, Phase H marked deferred, Architecture Notes data-strategy updated for paliad schema + office-scoped RLS.
  • .claude/CLAUDE.md refreshed with current feature list, env vars (DATABASE_URL, CALDAV_ENCRYPTION_KEY), and phase status.
  • README.md refreshed with current feature list, full migration inventory, env vars, and project layout.
  • This "Post-Integration Status" section added.

Still pending — requires head + m coordination (NOT in this Phase J task scope):

  • Add kanzlai.msbls.de domain to Paliad Dokploy compose with 301 redirect rule.
  • Stop and delete the KanzlAI Dokploy app.
  • Archive the m/KanzlAI-mGMT Gitea repo (set read-only / archived).
  • Merge-or-separate decision for mai.projects.kanzlai vs. mai.projects.paliad.
  • DROP SCHEMA kanzlai CASCADE on youpc Supabase after final verification.
  • Memory: write a consolidation episode in the paliad group and supersede KanzlAI episodes (noted as a followup for m).

These are ops actions with real blast radius (public domain cutover, shared-DB schema drop, repo archival) and should not run unattended from a documentation task.

Email gate: still hardcoded

The design §2 specified an env-configurable whitelist [@hoganlovells.com, @hlc.com, @hlc.de]. Current code (internal/handlers/auth.go:115) still hardcodes hoganlovells.com. Move to env config before HLC emails come online — trivial change, just hasn't happened yet.

Visibility model: verified in use across shipped phases

The paliad.can_see_akte(akte_id) predicate is the single source of truth and is reused by every RLS policy and mirrored by AkteService.GetByID at the application layer. FristService, TerminService, and ParteienService all route through AkteService.GetByID before operating on their own rows. No duplication. Architecture invariant held through Phases D, E, F.

CalDAV: manual Outlook plan still open

Phase F verified CalDAV against dav.msbls.de and Apple iCloud. HLC lives on Outlook + Exchange where CalDAV support is limited or off by default. The Phase K plan (EWS / Microsoft Graph backend behind the same sync abstraction) remains the fallback. Reassess after first real HLC user feedback.


End of design.