Files
teatea-pension/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md

8.6 KiB

Design

Architecture

This task keeps the app in a single Next.js project and continues the Drizzle/PostgreSQL-backed server domain layer inside modules/:

  • modules/core/server/: Drizzle connection, schema, session, permissions, audit helpers, and compatibility read models.
  • modules/auth/: UI and client interactions for setup, register, login, logout, and session state.
  • modules/elders/: elder schemas/types, validation, and CRUD UI components.
  • modules/settings/: role/account/organization/audit/status display and management components.
  • modules/facilities or existing page-local modules: room, bed, occupancy, admission, transfer, and discharge UI as the feature grows.
  • app/api/.../route.ts: Route Handlers for auth, session, elders, facilities, admissions, accounts, roles, organizations, status, and audit APIs.

The persistence boundary is now Drizzle/PostgreSQL, not JSON files. UI and route handlers should use domain helpers or direct Drizzle queries through getDatabase() in server-only modules. New mutation paths must not use writeData() or updateData().

Data Storage

The current persistent model is PostgreSQL with Drizzle schema in modules/core/server/schema.ts and generated migrations under drizzle/.

Important existing tables:

  • Identity and tenancy: accounts, sessions, organizations, memberships, join_requests, organization_invitations.
  • Authorization: roles, permissions, role_permissions.
  • Operations: elders, rooms, beds, admissions.
  • Governance and status: audit_logs, system_incidents, system_settings.

modules/core/server/store.ts should be treated as a compatibility read model only:

  • readData() may aggregate Drizzle rows into existing UI-friendly types while legacy pages are migrated.
  • writeData() and updateData() intentionally throw after the PostgreSQL migration.
  • New feature work should prefer focused query helpers so the compatibility model does not become the long-term domain layer.

API Contracts

Route Handlers return a consistent response shape:

type ApiResult<T extends Record<string, unknown>> =
  | ({ success: true; reason: string } & T)
  | { success: false; reason: string };

Existing or expected endpoints:

  • GET /api/auth/bootstrap: returns whether setup is required.
  • POST /api/auth/setup: creates the first platform admin, first organization, session, roles/membership, and audit log.
  • POST /api/auth/register: creates a user registration or invitation-based account flow where enabled.
  • POST /api/auth/login: validates credentials, creates a session, logs login.
  • POST /api/auth/logout: deletes current session cookie/session, logs logout.
  • GET /api/auth/session: returns current account, organization, membership, and permissions.
  • GET /api/elders: lists elder profiles for the active organization.
  • POST /api/elders: creates an elder profile and optional active admission.
  • PATCH /api/elders/[id]: updates an elder profile.
  • DELETE /api/elders/[id]: deletes an elder profile.
  • GET /api/facilities/rooms: lists rooms for the active organization.
  • POST /api/facilities/rooms: creates room records when facility management UI exposes this.
  • GET /api/facilities/beds: lists beds and current occupancy.
  • POST /api/facilities/beds: creates bed records when facility management UI exposes this.
  • GET /api/admissions: lists admission and transfer history.
  • POST /api/admissions: admits or transfers an elder into an available bed.
  • Planned: PATCH /api/admissions/[id] or equivalent mutation endpoint for transfer/discharge if POST cannot express the workflow cleanly.
  • GET /api/settings/accounts: lists accounts for authorized roles.
  • GET /api/settings/roles: lists built-in and organization roles.
  • GET /api/settings/permissions: lists permission coverage.
  • GET /api/audit-logs: lists recent audit events.

Authentication

Sessions use an HTTP-only cookie named teatea_session. Route Handlers read and write cookies with await cookies() from next/headers, matching current Next.js behavior.

Passwords are never stored in plain text. The current implementation uses Node built-in crypto with per-account salt and scryptSync, which is acceptable for this local MVP until a dedicated auth library is introduced in a separate task.

Protected app routes should use server session state where practical instead of client-only localStorage guards. Server-rendered pages that depend on session cookies must opt out of static prerendering with export const dynamic = "force-dynamic" where needed.

Permissions

Permissions and role definitions live in the core server/shared type boundary:

  • Platform: platform:manage, organization:read, organization:manage.
  • Account/role/security: account:read, account:manage, role:read, role:manage, permission:read, audit:read.
  • Operations: facility:read, facility:manage, admission:read, admission:manage, elder:read, elder:create, elder:update, elder:delete.
  • Status: incident:read, incident:manage.

Route Handlers call a shared requirePermission(permission) helper. This helper returns an authenticated context or a structured forbidden/unauthorized response and writes denied audit entries.

Admission Transactions

Bed/admission mutations must run inside a Drizzle transaction.

Admit flow:

  1. Validate active organization and admission:manage.
  2. Validate elder belongs to the active organization.
  3. Validate target bed belongs to the active organization and has status available.
  4. Close or transfer any active admission for that elder if the operation is a transfer.
  5. Insert a new admissions row with status active.
  6. Set target bed status to occupied.
  7. Set elder status to active.
  8. Record an audit log.

Transfer flow:

  1. Find active admission for the elder.
  2. Set previous admission status to transferred and dischargedAt to now.
  3. Set previous bed status to available.
  4. Insert the new active admission and occupy the target bed.
  5. Record an audit log.

Discharge flow:

  1. Find active admission.
  2. Set admission status to discharged and dischargedAt to now.
  3. Set bed status to available.
  4. Set elder status to discharged or another explicitly selected status.
  5. Record an audit log.

All conflict checks must return structured API failures rather than partially mutating state.

UI Flow

The app should remain an operational workspace: dense, restrained, and action-oriented.

Elder page:

  • Server Component loads initial elders and available beds.
  • Client component handles create/edit/delete forms and refreshes after mutations.
  • Controls are disabled or hidden based on current permissions, while APIs still enforce permission checks.

Bed/admission page:

  • Replace raw API placeholders with page-local controls.
  • Use tabs or segmented navigation for overview, bed status, admissions/history, and management actions.
  • Place admit/transfer/discharge actions inside the bed/admission workspace, not as a dead global top-bar button.
  • Show occupancy metrics, active admissions, room/bed tables, and history from Drizzle-backed data.

Dashboard:

  • Replace hard-coded counters for implemented domains with Drizzle-backed data.
  • Use a standard chart library for selected charts such as occupancy distribution or admission activity.
  • Keep chart usage modest; tables and status lists remain the primary record surfaces.

Screenshot feedback:

  • Move page-level secondary navigation into tabs where appropriate.
  • Remove oversized intro/hero blocks from routine operational pages.
  • Remove redundant header action controls that are not wired to the current page workflow.

Compatibility

Static module pages not in scope remain untouched except for auth/layout integration. Existing visual design should be preserved: dense operational screens, restrained cards/tables, project UI adapters under components/ui/*, and Tailwind-compatible layout.

Risks and Rollback

  • Drizzle schema and migrations are now the persistence source of truth; mismatches between schema and migrations can block deployment. Rollback point: keep migration changes separate from UI-only work.
  • Admission mutations touch multiple tables. Rollback point: keep transaction helpers isolated and temporarily render read-only admission data if mutation UI is unstable.
  • Replacing global header actions can affect user navigation habits. Rollback point: remove only the dead top-bar "入住" button while keeping the sidebar and pages stable.
  • Adding a chart library increases client bundle size. Rollback point: limit chart usage to one focused client component and keep tables as the fallback data surface.