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.
46 KiB
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.
Why not keep KanzlAI as-is and link to it?
- 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_visibleis the partner override for cross-office matters of firm-wide interest (e.g., a strategic UPC test case).collaboratorsis 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 withUSING (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.comSupabase 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.usersif 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 onpaliad.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_collaboratorstable with email + scoped RLS. Don't build now. - Read-only Akten archive after closure: add
is_archivedflag, 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:
- Create
paliadschema in the same youpc Supabase instance (no new project). - Move Paliad's 3 existing feedback tables into
paliad.*(CREATE in new schema, COPY data, drop old). - Port KanzlAI's tables into
paliad.*, dropping multi-tenancy fields and adding visibility columns:cases→paliad.akten(German throughout: table name, Go structAkte, URL/akten, UI label "Akten")parties→paliad.parteiendeadlines→paliad.fristenappointments→paliad.terminedocuments→paliad.dokumentecase_events→paliad.akten_eventsnotes→paliad.notizen- All drop
tenant_id, all addakte_idFK topaliad.akten, all gain visibility viapaliad.can_see_akte(akte_id)in RLS. paliad.aktenitself addsowning_office,collaborators uuid[],firm_wide_visible booleanper §2.- Reference tables:
proceeding_types,deadline_rules,holidays→paliad.*. RLS-on but permissive (no user data). - New:
paliad.usersper §2.
- RLS enabled on all paliad tables with real visibility policies per §2 (not just permissive-authenticated).
- Retire
kanzlai.*schema once cutover is verified (DROP SCHEMA kanzlai CASCADEin 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 withcases(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_rulestable has 32 UPC + 4 ZPO rules (more than Paliad's hardcodedinternal/calc/deadline_rules.go). Port the data, drop Paliad's hardcoded rules. - Reuse the holiday data: KanzlAI's
holidaystable 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/migrateas a sidecar in the Docker entrypoint. Migrations live inmigrations/(top-level, notdocs/migrations/— that path is wrong; move them). Numbered00N_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'sinternal/calc/deadline_rules.gohardcoded 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_eventsaudit 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 527–534). 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 463–483): 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
/fristenwhich 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— primarypatholo.de— legacy redirect (already configured)patholo.msbls.de— internal preview (already configured)- Add:
kanzlai.msbls.de→ 301 redirect topaliad.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
mainonm/paliad→ Gitea webhook → Dokploy auto-deploy. - Dockerfile changes: add migration step to entrypoint (run
migrate upagainstDATABASE_URLbefore 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:
- Apply migrations (Phase A).
- Ship phases B–H to
paliad.de. - Verify functionality.
- Flip
kanzlai.msbls.deto 301-redirect topaliad.de(Phase J). - 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
paliadschema in youpc Supabase instance. - Add
golang-migrateto 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 intopaliad.*. - Create
paliad.userstable withoffice,practice_group,rolecolumns per §2. - Port KanzlAI tables →
paliad.*migrations with German names:proceeding_types,deadline_rules,holidays,akten(renamed fromcases, withowning_office+collaborators uuid[]+firm_wide_visiblecolumns),parteien,fristen,termine,dokumente,akten_events,notizen. Droptenant_ideverywhere; addcreated_byFK topaliad.users.id. - Create
paliad.can_see_akte(akte_id uuid)SQL function per §2. - RLS policies per §2:
akten_select/insert/update/deleteusing the visibility predicate; child-row policies (fristen, termine, parteien, dokumente, akten_events, notizen) usingpaliad.can_see_akte(akte_id). Reference tables RLS-on butUSING (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 oncollaboratorsfor= ANYlookups. - Seed: 6 proceeding types, 32 UPC + 4 ZPO rules, 68 holidays (port directly from KanzlAI seed).
- Seed
paliad.userswith cronus's test user asadminfor local dev (real users register via the app).
Acceptance:
\dt paliad.*shows ≥12 tables (includingusers).- 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 upruns 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 topaliad.*— don't rely on session-levelsearch_pathper audit §2.8). - Port
internal/services/holidays.gofrom KanzlAI (withsync.RWMutexfix 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— bootstrappaliad.usersrow fromauth.users, get current user from request context, list users. - Add
internal/services/akte_service.go(formerlycase_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-rulesreturns rules from DB.POST /api/fristen/calculateworks with DB rules (proves Phase A schema ↔ Phase B services wiring).GET/POST /api/aktenworks (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.gohardcoded data. - Update
internal/handlers/fristenrechner.goto call DB-backed services from Phase B. - Verify existing
/tools/fristenrechnerUI is byte-identical (no UX regression). - Update
internal/calc/deadlines.go: delete logic that's now inservices/deadline_calculator.go; thin shim if needed for the existing JSON shape.
Acceptance:
/tools/fristenrechnerpage 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 currentauth.uid(), creates thepaliad.usersrow. - 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: ifpaliad.usersrow 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_officedefaults 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_eventsrow. - 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+ onakten-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.goported from KanzlAI with credential encryption fix (audit §1.3): useCALDAV_ENCRYPTION_KEYenv 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 newpaliad.user_caldav_configtable). - 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.goported and simplified (no tenant; uses visibility predicate). Returnsfrist_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
/dashboardwith 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.goported 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_KEYis 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:
notizentable 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
NotizenListTSX 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.mdper §5 of this doc. - Add
kanzlai.msbls.dedomain to Paliad Dokploy compose with 301 redirect rule (Traefik label or middleware). - Stop and delete KanzlAI Dokploy app.
- Archive
m/KanzlAI-mGMTGitea repo (set to read-only / archived). - Update
mai.projects: optionally mergekanzlaiproject intopaliad(decide: keep history separate or unify). - Memory: write episode in
paliadgroup documenting the consolidation, supersede KanzlAI episodes. - Drop
kanzlaischema from Supabase:DROP SCHEMA kanzlai CASCADEafter final verification that no Paliad code references it.
Acceptance:
kanzlai.msbls.deredirects topaliad.dewith 301.- KanzlAI Dokploy app gone.
- KanzlAI repo archived.
- Roadmap doc reflects the new strategy.
kanzlaischema 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 1–2 phases per day with one coder, this completes in 2–3 working weeks.
9. Risks & unknowns
Risks
-
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.
-
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. -
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. -
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. -
CalDAV encryption key rotation. If
CALDAV_ENCRYPTION_KEYis 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. -
KanzlAI-derived German Umlaut typos in audit §2.10. Don't carry them forward. Style check during each frontend port phase.
-
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.deand 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. -
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_groupas 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 3–5 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 A–J 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 A–C (foundation, including the security-critical visibility model), hand off D–I 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 D–I 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.mdrewritten 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 forpaliadschema + office-scoped RLS..claude/CLAUDE.mdrefreshed with current feature list, env vars (DATABASE_URL,CALDAV_ENCRYPTION_KEY), and phase status.README.mdrefreshed 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.dedomain to Paliad Dokploy compose with 301 redirect rule. - Stop and delete the KanzlAI Dokploy app.
- Archive the
m/KanzlAI-mGMTGitea repo (set read-only / archived). - Merge-or-separate decision for
mai.projects.kanzlaivs.mai.projects.paliad. DROP SCHEMA kanzlai CASCADEon youpc Supabase after final verification.- Memory: write a consolidation episode in the
paliadgroup 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.