From 68628f886142b2390544a39c237957e87e256227 Mon Sep 17 00:00:00 2001 From: zemion Date: Mon, 6 Jul 2026 14:28:43 +0200 Subject: [PATCH] Sync Repo-ARCHITECTURE-CONTRACTS from project files --- Repo-ARCHITECTURE-CONTRACTS.-.md | 508 +++++++++++++++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 Repo-ARCHITECTURE-CONTRACTS.-.md diff --git a/Repo-ARCHITECTURE-CONTRACTS.-.md b/Repo-ARCHITECTURE-CONTRACTS.-.md new file mode 100644 index 0000000..9e9041a --- /dev/null +++ b/Repo-ARCHITECTURE-CONTRACTS.-.md @@ -0,0 +1,508 @@ + + +> Mirrored from `/mnt/DATA/git/comiaunicaty/ARCHITECTURE_CONTRACTS.md`. +> Origin: `repository`. +> Active tasks and changing state belong in Gitea issues; this wiki page is durable project context. + +--- +# ARCHITECTURE_CONTRACTS.md + +## Architecture overview + +The same FastAPI server can operate as: + +```text +local group server +home server +both +``` + +The React frontend talks primarily to one selected home server. The home server stores local memberships and remote server connection configuration. It aggregates structured objects from connected remote group servers. + +```text +Browser/PWA + | + | HTTPS/API + HttpOnly session cookie + v +Home Server + | owns home profile, devices, preferences, remote config + | fetches remote structured sync using scoped server-side tokens + v +Remote Group Servers + | own canonical groups and group data + | expose /.well-known/group-platform.json and /api/sync +``` + +No server-to-server federation is required. Remote group servers do not know about each other. + +## Browser storage policy + +Allowed in localStorage: + +- last selected home server URL; +- UI preferences; +- theme choice; +- non-sensitive onboarding flags; +- cached non-sensitive IDs for convenience. + +Not allowed in localStorage: + +- raw invite tokens after claim; +- session tokens; +- remote server access tokens; +- recovery tokens; +- admin credentials. + +Use HttpOnly cookies for sessions. Use server-side storage for remote connection credentials. + +## Core entity relationships + +```text +HomeProfile 1..n HomeDevice +HomeProfile 1..n RecoveryMethod +HomeProfile 1..n RemoteServerConnection +HomeProfile 0..n Member via Member.home_profile_id + +Group 1..n Member +Group 1..n MemberInvite +Group 1..n Announcement +Group 1..n Event +Event 1..n EventRsvp +Group 1..n Task +Group 1..n Poll +Poll 1..n PollOption +PollOption 1..n PollVote +Group 1..n FileAsset +Group 1..n Thread +Thread 1..n Message + +Member 1..n MemberDevice +Member 1..n ActionItem +Member 1..n Notification + +RemoteServerConnection 1..n RemoteMembership +RemoteServerConnection 1..n RemoteSyncCursor +RemoteServerConnection 1..n RemoteCachedObject +``` + +A `Member` can exist without a `HomeProfile`. This is required for accountless invite-link joining. Later, a member may be attached to a home profile. + +## Suggested SQLAlchemy fields + +Use exact names where practical. + +### HomeProfile + +```text +id: UUID +primary_display_name: string +created_at: datetime +updated_at: datetime +last_seen_at: datetime nullable +status: active/disabled +``` + +### HomeDevice + +```text +id: UUID +home_profile_id: UUID nullable +member_id: UUID nullable +label: string +user_agent_summary: string +created_at: datetime +last_seen_at: datetime +revoked_at: datetime nullable +trust_level: invite_member/claimed_browser/verified/passkey_ready +``` + +### RecoveryMethod + +```text +id: UUID +home_profile_id: UUID +kind: email/phone/recovery_code/passkey +value_hash: string nullable +display_hint: string +verified_at: datetime nullable +created_at: datetime +``` + +### Session + +```text +id: UUID +home_profile_id: UUID nullable +member_id: UUID nullable +home_device_id: UUID nullable +member_device_id: UUID nullable +csrf_token_hash: string +expires_at: datetime +created_at: datetime +revoked_at: datetime nullable +``` + +### Group + +```text +id: UUID +server_origin: string +name: string +description: text +visibility: private/public/listed +legacy_channel_status: none/transition/legacy +transition_deadline: date nullable +created_at: datetime +updated_at: datetime +archived_at: datetime nullable +``` + +### Member + +```text +id: UUID +group_id: UUID +home_profile_id: UUID nullable +display_name: string +role: owner/admin/moderator/member/guest +status: invited/opened/joined/verified/suspended/left +joined_at: datetime nullable +last_seen_at: datetime nullable +notification_enabled_at: datetime nullable +``` + +### MemberInvite + +```text +id: UUID +group_id: UUID +member_id: UUID nullable +created_by_member_id: UUID +label: string +scope: specific_member/open_seat/admin_invite +permission_role: guest/member/admin +plain_token_display_once: not persisted +token_hash: string +expires_at: datetime nullable +max_uses: int +use_count: int +consumed_at: datetime nullable +revoked_at: datetime nullable +created_at: datetime +``` + +### Announcement + +```text +id: UUID +group_id: UUID +author_member_id: UUID +title: string +body: text +priority: normal/urgent +official: bool +requires_ack: bool +created_at: datetime +updated_at: datetime +``` + +### Event + +```text +id: UUID +group_id: UUID +created_by_member_id: UUID +title: string +description: text nullable +starts_at: datetime +ends_at: datetime nullable +location_name: string nullable +location_address: string nullable +rsvp_required: bool +changed_at: datetime nullable +created_at: datetime +updated_at: datetime +``` + +### EventRsvp + +```text +id: UUID +event_id: UUID +member_id: UUID +status: yes/no/maybe/unknown +note: text nullable +updated_at: datetime +``` + +### Task + +```text +id: UUID +group_id: UUID +created_by_member_id: UUID +assigned_to_member_id: UUID nullable +title: string +description: text nullable +due_at: datetime nullable +status: open/done/cancelled +created_at: datetime +updated_at: datetime +``` + +### Poll / PollOption / PollVote + +```text +Poll: id, group_id, title, description, closes_at, status, created_by_member_id, created_at +PollOption: id, poll_id, label, position +PollVote: id, poll_id, option_id, member_id, created_at +``` + +### FileAsset + +```text +id: UUID +group_id: UUID +uploaded_by_member_id: UUID +filename_original: string +filename_stored: string +content_type: string +size_bytes: int +storage_path: string +visibility: members/admins/public_link +created_at: datetime +``` + +### Thread / Message + +```text +Thread: id, group_id, title, kind discussion/question/archive, created_by_member_id, created_at, updated_at +Message: id, thread_id, author_member_id, body, created_at, edited_at nullable, deleted_at nullable +``` + +### ActionItem + +```text +id: UUID +home_profile_id: UUID nullable +member_id: UUID nullable +remote_connection_id: UUID nullable +source_type: local/remote +source_server_origin: string +source_group_id: string +source_group_name: string +type: rsvp_required/vote_required/task_assigned/event_changed/file_ack/direct_mention/admin_request +status: open/done/dismissed +priority: low/normal/high/urgent +title: string +summary: text +object_type: event/task/poll/file/announcement/message +object_id: string +due_at: datetime nullable +created_at: datetime +updated_at: datetime +``` + +### RemoteServerConnection + +```text +id: UUID +home_profile_id: UUID +server_origin: string +server_name: string +api_base: string +protocol_version: string +capabilities_json: json +access_token_encrypted: string +status: active/error/revoked +last_sync_at: datetime nullable +last_error: text nullable +created_at: datetime +updated_at: datetime +``` + +### RemoteCachedObject + +```text +id: UUID +remote_connection_id: UUID +object_type: action/event/announcement/file/thread +remote_id: string +group_remote_id: string +group_name: string +payload_json: json +updated_at_remote: datetime nullable +cached_at: datetime +``` + +## API contracts + +Use JSON. Use ISO 8601 datetimes. Use consistent error response: + +```json +{ + "error": { + "code": "permission_denied", + "message": "You do not have permission to do that.", + "details": {} + } +} +``` + +### Invite preview + +```http +GET /api/join/{token}/preview +``` + +Response: + +```json +{ + "group": { + "id": "...", + "name": "FC Kreuzberg U12 Parents", + "description": "Planning, matches, files, and announcements." + }, + "invite": { + "label": "Parent invite", + "expires_at": null, + "role": "member" + }, + "preview": { + "announcements": [], + "events": [] + } +} +``` + +### Claim invite + +```http +POST /api/auth/invite/{token}/claim +``` + +Body: + +```json +{ + "display_name": "Anna Müller", + "device_label": "iPhone Safari" +} +``` + +Response sets session cookie and returns: + +```json +{ + "member": {}, + "group": {}, + "next_steps": ["save_access", "enable_notifications"] +} +``` + +### Home dashboard + +```http +GET /api/home +``` + +Response: + +```json +{ + "profile": {}, + "sections": { + "needs_me": [], + "today": [], + "changed": [], + "official_updates": [], + "catch_up": [] + }, + "connections": [] +} +``` + +### Sync endpoint + +```http +GET /api/sync?since=cursor +Authorization: Bearer +``` + +Response: + +```json +{ + "cursor": "next-cursor", + "server_time": "2026-06-29T10:00:00Z", + "actions": [], + "events": [], + "announcements": [], + "files": [], + "threads": [] +} +``` + +## Remote aggregation behavior + +The home server stores a cursor per remote connection. Sync should be idempotent. + +Remote cached objects should include enough payload to render in the Home dashboard without live remote calls every time. When a user performs a write action on a remote object, the MVP can open the remote group page in a new tab if write-through is not implemented. Implement write-through for RSVP if feasible. + +## Derived action logic + +Generate local action items from structured state: + +- Event with `rsvp_required=true` and no RSVP by current member → `rsvp_required`. +- Task assigned to current member and status `open` → `task_assigned`. +- Poll open with no vote by current member → `vote_required`. +- Event changed after member's last seen time → `event_changed`. +- Announcement requires acknowledgement and no acknowledgement exists → `file_ack` or future `announcement_ack`. + +It is acceptable to recompute on request in MVP, but persist/cache for remote sync. + +## Permissions contract + +Implement a simple helper: + +```python +require_role(member, group, min_role="admin") +can(member, action, resource) +``` + +Role hierarchy: + +```text +guest < member < moderator < admin < owner +``` + +Make role checks visible in route handlers or service methods. + +## Development remote-server demo + +Provide a way to run two local instances: + +```text +home server: http://localhost:8000 +remote server: http://localhost:8001 +``` + +Options: + +- docker-compose service duplication; or +- README command using different ports and DB paths. + +Seed each with distinct server names and groups. Show how to create a remote connection token on server 8001 and connect it from server 8000. + +## Production limitations to document + +Document these limitations in README: + +- Dev email sends to console unless SMTP configured. +- Passkey implementation may be development/pluggable if not fully implemented. +- Web push may be a future adapter. +- No full federation. +- No end-to-end encryption. +- Remote sync tokens require secure secret management in production. +- File storage is local filesystem in dev; production should use S3-compatible storage or mounted volume.