From a1dedc966c3ccfc4acd6795ecbe75ce874c75b3a Mon Sep 17 00:00:00 2001 From: Janarthanan Soundhararajan Date: Sat, 29 Aug 2026 17:08:58 +0530 Subject: [PATCH 1/2] docs: define journeys rules and domain decisions --- docs/README.md | 9 +- docs/business-rules.md | 317 +++++++++++ docs/content-opportunities.md | 52 +- docs/domain-decisions.md | 667 +++++++++++++++++++++++ docs/domain-model-current.md | 806 +++++++++++++++++++++++++++ docs/use-cases.md | 991 ++++++++++++++++++++++++++++++++++ docs/user-journeys.md | 565 +++++++++++++++++++ 7 files changed, 3403 insertions(+), 4 deletions(-) create mode 100644 docs/business-rules.md create mode 100644 docs/domain-decisions.md create mode 100644 docs/domain-model-current.md create mode 100644 docs/use-cases.md create mode 100644 docs/user-journeys.md diff --git a/docs/README.md b/docs/README.md index 6102de0..6b49dbe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,9 +26,12 @@ Statements about current behavior should be verifiable in code. Proposed behavio | [Feature inventory](./feature-inventory.md) | Current, partial, proposed, and later capabilities | Initial draft | | [Repository architecture](./repository-architecture.md) | Turborepo structure, application boundaries, dependency rules, and package strategy | Initial draft | | [ADR 0001: Modular monolith](./adr/0001-adopt-modular-monolith.md) | Accepted decision for structuring the product application as business modules | Accepted | -| User journeys | End-to-end direct-booking and group-scheduling journeys | Planned | -| Use cases | Actors, permissions, preconditions, alternative flows, and failures | Planned | -| Domain model | Current and proposed ER models, invariants, and lifecycle states | Planned | +| [User journeys](./user-journeys.md) | Actors, current experience, target outcomes, and cross-journey requirements | Initial draft | +| [Use cases](./use-cases.md) | Preconditions, authorization, success, failures, postconditions, and preliminary rules | Initial draft | +| [Business rules](./business-rules.md) | Authoritative invariants, current enforcement, target rules, and test implications | Initial draft | +| [Current domain model](./domain-model-current.md) | Existing Prisma entities, ER diagram, keys, cardinality, constraints, indexes, and integrity gaps | Current-state analysis | +| [Proposed domain decisions](./domain-decisions.md) | Recommended choices and trade-offs that must be reviewed before the proposed ER model | Recommended for review | +| Proposed domain model | Target entities, aggregate boundaries, lifecycle states, constraints, and migration path | Planned | | System architecture | Runtime components, integrations, security boundaries, and sequences | Planned | | Deployment architecture | Environments, domains, infrastructure, and operational concerns | Planned | | Roadmap | Vertical delivery milestones and dependencies | Planned | diff --git a/docs/business-rules.md b/docs/business-rules.md new file mode 100644 index 0000000..bdb18e1 --- /dev/null +++ b/docs/business-rules.md @@ -0,0 +1,317 @@ +# Business Rules + +**Document status:** Initial draft +**Scope:** Current enforcement, target invariants, and unresolved policy decisions + +## Purpose + +A business rule is a statement that must remain true regardless of which page, API, Server Action, background job, or integration performs an operation. + +For example: + +> Only an open poll accepts responses. + +That rule should hold whether a response comes from the current web form, a future mobile client, an API, or an automated import. + +This catalogue gives rules stable identifiers so that use cases, ER constraints, implementation code, and tests can refer to the same requirement. + +## Rule status + +| Status | Meaning | +| --- | --- | +| **Current** | The rule is enforced by the current implementation. | +| **Partial** | Some enforcement exists, but a bypass, race, or inconsistency remains. | +| **Proposed** | The rule belongs to the accepted target behavior but is not implemented. | +| **Unresolved** | Product or policy decision is required before accepting the rule. | + +## Enforcement layers + +| Layer | Appropriate use | +| --- | --- | +| Database constraint | Identity, uniqueness, referential integrity, required values, simple row-level checks | +| Database transaction | Multi-record transitions that must be atomic | +| Application/domain service | Contextual rules, authorization, state transitions, calculations, and policies | +| Background processor | Deadlines, retries, reminders, and durable external work | +| External provider protocol | Provider event identity, delivery semantics, and provider-specific constraints | +| UI | Guidance and early feedback only; never the sole enforcement layer | + +Important rules often require more than one layer. An application can provide a friendly “slug already exists” message while a database unique constraint remains the final concurrency-safe enforcement. + +## Identity and onboarding + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-ID-001 | A public username is unique after the accepted normalization policy. | Partial | Database uniqueness exists; normalization/reserved-name policy is incomplete | UC-ID-001 | +| BR-ID-002 | A user has one current onboarding state and repeating completion does not duplicate default resources. | Proposed | Application service plus database uniqueness | UC-ID-001 | +| BR-ID-003 | OAuth provider account identity is unique by provider and provider account ID. | Current | Database composite unique constraint | Authentication | +| BR-ID-004 | One email address maps to at most one current user when an email is present. | Current | Database unique nullable column | Authentication | +| BR-ID-005 | Authentication consent and calendar-access consent are separate decisions. | Proposed | OAuth scope/configuration boundary | UC-ID-001, calendar connection | + +### Notes + +- Username uniqueness currently exists, but case normalization and reserved words such as route names still require policy. +- OAuth login acts as account creation on first use; a separate password-based registration flow is not required by the current product. + +## Authorization and tenancy + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-AUTH-001 | Every protected mutation derives authenticated identity server-side. | Current | Server Action authentication | UC-ID-001, UC-SCH-001, UC-SCH-002, UC-POL-001 | +| BR-AUTH-002 | A resource identifier identifies a record but does not grant mutation permission. | Partial | Ownership checks exist for several actions; not yet governed consistently | All mutations | +| BR-AUTH-003 | A user may mutate a personally owned resource only when its owner ID matches the authenticated identity. | Current for implemented owner actions | Application query filter | UC-SCH-001, UC-SCH-002 | +| BR-AUTH-004 | Public booking requires no account but must validate resource activity, ownership relationship, input, and abuse policy. | Partial | Resource/input validation exists; rate limiting and full slot validation do not | UC-BKG-001 | +| BR-AUTH-005 | Accountless meeting management requires an unguessable, revocable authorization mechanism; meeting ID alone is insufficient. | Proposed | Hashed management token plus application policy | UC-MTG-001, UC-MTG-002 | +| BR-AUTH-006 | Workspace-scoped mutations require current membership and sufficient permission. | Proposed | Membership query/policy and database relationships | Future workspace use cases | +| BR-AUTH-007 | Automatic actions operate only under a policy previously configured by an authorized actor. | Proposed | Durable policy plus background processor | UC-POL-003 | + +## Recurring availability + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-SCH-001 | Availability time values use valid 24-hour `HH:mm` format. | Current | Zod application validation | UC-SCH-001 | +| BR-SCH-002 | Every availability window has `startTime < endTime`. | Partial | Intended by domain; not currently enforced by schema/action | UC-SCH-001 | +| BR-SCH-003 | A user has at most one recurring availability record per weekday. | Current | Database composite unique constraint | UC-SCH-001 | +| BR-SCH-004 | A disabled day produces no booking candidates even if fallback window data is stored. | Current | Availability engine checks `isAvailable` | UC-SCH-001, UC-BKG-001 | +| BR-SCH-005 | Saving a weekly schedule does not leave a partially updated week. | Proposed | Database transaction | UC-SCH-001 | +| BR-SCH-006 | Windows for the same day follow a defined overlap policy. | Unresolved | Application validation; possible normalization | UC-SCH-001 | +| BR-SCH-007 | Candidate generation applies event duration and configured pre/post buffers. | Partial | Engine supports buffers; booking UI currently supplies zero | UC-SCH-002, UC-BKG-001 | + +### Important distinction + +Storing fallback window data for a disabled day is not itself a contradiction. The authoritative availability decision combines `isAvailable` and the window collection. This must be documented because reading `startTime` alone would produce the wrong result. + +## Event types + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-EVT-001 | Event-type slug is unique within its current user-owner scope. | Current | Database composite unique constraint | UC-SCH-002 | +| BR-EVT-002 | Slug contains only lowercase letters, digits, and hyphens and is 1–50 characters. | Current | Zod application validation | UC-SCH-002 | +| BR-EVT-003 | Event duration is between 5 and 480 minutes. | Current at application boundary | Zod validation; no database check | UC-SCH-002, UC-BKG-001 | +| BR-EVT-004 | Buffers are non-negative. | Current at application boundary | Zod validation; no database check | UC-SCH-002, UC-BKG-001 | +| BR-EVT-005 | Archived event types reject new bookings. | Current | Booking application service | UC-BKG-001 | +| BR-EVT-006 | Public booking uses persisted event-type duration and ownership rather than client claims. | Current | Booking application service | UC-BKG-001 | +| BR-EVT-007 | Deleting an event type with existing bookings follows an explicit retention policy. | Unresolved | Current foreign key cascades bookings | Event-type deletion, meeting history | + +### Deletion policy warning + +The current schema cascades `EventType` deletion to its bookings. That may be convenient during development, but it could erase historical scheduled records. The proposed domain model must decide whether event types are archived rather than physically deleted after use. + +## Direct booking + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-BKG-001 | Booking event type belongs to the selected host. | Current | Application service compares persisted IDs | UC-BKG-001 | +| BR-BKG-002 | Booking end derives from persisted event duration. | Current | Application service | UC-BKG-001 | +| BR-BKG-003 | Pending and accepted bookings block overlapping reservations; cancelled bookings do not. | Partial | Application overlap query; not concurrency-safe | UC-BKG-001, UC-MTG-001 | +| BR-BKG-004 | A submitted slot must belong to the host's valid generated availability at submission time. | Proposed | Application/domain service with authoritative recomputation | UC-BKG-001 | +| BR-BKG-005 | A booking cannot begin in the past and follows configured minimum/maximum notice. | Proposed | Application/domain service | UC-BKG-001 | +| BR-BKG-006 | A booking is created at most once for one logical submission. | Proposed | Idempotency key plus database uniqueness | UC-BKG-001 | +| BR-BKG-007 | Calendar UID is stable and unique for the scheduled commitment. | Partial | Implemented on email-invitation feature branch; database uniqueness proposed/current with that change | UC-BKG-001, UC-MTG-002 | +| BR-BKG-008 | Booking success remains authoritative when confirmation delivery fails. | Partial | Implemented on email-invitation feature branch; durable delivery state not persisted | UC-BKG-001 | +| BR-BKG-009 | Guest email is syntactically valid and guest name satisfies accepted limits. | Current | Zod validation | UC-BKG-001 | +| BR-BKG-010 | Guest timezone has valid IANA semantics, not merely a non-empty value. | Partial | Currently checks only non-empty string | UC-BKG-001 | + +### Concurrency invariant + +The desired invariant is: + +```text +For one host, two blocking booking intervals may not overlap. +``` + +The current sequence is: + +```text +check for overlap -> create booking +``` + +Two requests can pass the check before either inserts. The final design must select a PostgreSQL-compatible enforcement strategy and document its interaction with Prisma and booking status. + +## Poll lifecycle and candidates + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-POL-001 | A published poll contains at least one candidate. | Current at creation boundary | Zod minimum plus nested create | UC-POL-001 | +| BR-POL-002 | Every candidate belongs to exactly one poll. | Current | Required foreign key | UC-POL-001, UC-POL-002 | +| BR-POL-003 | Poll slug is unique under the current public URL design. | Current | Database unique constraint | UC-POL-001 | +| BR-POL-004 | Poll and initial candidates are created atomically. | Current | Prisma nested create | UC-POL-001 | +| BR-POL-005 | Poll has an explicit lifecycle: draft, open, finalized, expired, or cancelled. | Proposed | Enum/state-transition service | UC-POL-001, UC-POL-002, UC-POL-003 | +| BR-POL-006 | Only open polls accept new or revised responses. | Proposed | Application service | UC-POL-002 | +| BR-POL-007 | A finalized poll references exactly one candidate belonging to that poll. | Proposed | Composite relationship/transaction plus application rule | UC-POL-003 | +| BR-POL-008 | A poll produces at most one active resulting meeting. | Proposed | Database uniqueness plus transaction | UC-POL-003 | +| BR-POL-009 | Candidate duration and organizer timezone are explicit. | Proposed | Required model fields and validation | UC-POL-001 | +| BR-POL-010 | Candidate mutation is allowed only in lifecycle states defined by policy. | Proposed | State-transition service | UC-POL-001, UC-POL-003 | +| BR-POL-011 | Poll deadline, when present, prevents responses after its effective instant. | Proposed | Application service plus background transition | UC-POL-002, UC-POL-003 | +| BR-POL-012 | A candidate cannot end before or at its start. | Current structurally for generated one-hour candidates; proposed as general constraint | Application generation; future DB/application check | UC-POL-001 | + +## Poll participants and responses + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-RES-001 | A response belongs to one poll participant and one poll. | Proposed | Participant/response relationship | UC-POL-002 | +| BR-RES-002 | A participant has at most one effective preference per candidate. | Partial | Current unique constraint uses candidate and case-sensitive free-form name | UC-POL-002 | +| BR-RES-003 | Every submitted candidate belongs to the same poll as the response. | Partial | Foreign keys exist independently; submission action does not verify cross-poll consistency | UC-POL-002 | +| BR-RES-004 | Unanswered is distinct from `NO`. | Proposed | Nullable/absent response semantics and application policy | UC-POL-002 | +| BR-RES-005 | Revising a response replaces one participant's effective preferences atomically. | Partial | Transaction exists; identity relies on case-insensitive name deletion | UC-POL-002 | +| BR-RES-006 | Private invitation and edit tokens are stored as hashes, expire or revoke according to policy, and are never logged in plaintext. | Proposed | Token service and database fields | UC-POL-002 | +| BR-RES-007 | Required and optional participant classifications affect finalization according to explicit policy. | Proposed | Polling domain service | UC-POL-003 | +| BR-RES-008 | Aggregate visibility before response is controlled by poll policy. | Proposed | Query authorization/presentation policy | UC-POL-002 | +| BR-RES-009 | One person's display-name collision does not replace another person's response. | Proposed | Durable participant identity | UC-POL-002 | + +### Current integrity gap + +The current vote record stores both `pollId` and `timeSlotId`, but the database does not guarantee that the referenced time slot belongs to the same poll. Both foreign keys can be individually valid while their combination is invalid. + +The proposed model must enforce or structurally eliminate this mismatch. + +## Meeting lifecycle + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-MTG-001 | A meeting records whether it originated from direct booking, poll finalization, or another accepted source. | Proposed | Domain model | UC-BKG-001, UC-POL-003 | +| BR-MTG-002 | A meeting follows an explicit state machine rather than arbitrary status updates. | Proposed | Domain transition service | UC-MTG-001, UC-MTG-002 | +| BR-MTG-003 | Only meetings in cancellable states may transition to cancelled. | Proposed | Domain transition service | UC-MTG-001 | +| BR-MTG-004 | Cancellation records actor, time, and optional reason. | Proposed | Required lifecycle event/audit fields | UC-MTG-001 | +| BR-MTG-005 | Cancelled meetings stop blocking availability according to policy. | Current for booking conflict query; proposed for unified meeting model | Query/domain rule | UC-MTG-001 | +| BR-MTG-006 | Rescheduling preserves one authoritative meeting identity. | Proposed | Transaction and domain model | UC-MTG-002, UC-MTG-003 | +| BR-MTG-007 | Rescheduling retains the previous schedule in history. | Proposed | Revision or lifecycle-event model | UC-MTG-002, UC-MTG-003 | +| BR-MTG-008 | Concurrent updates do not silently overwrite a newer meeting version. | Proposed | Optimistic concurrency/version field | UC-MTG-001, UC-MTG-002 | +| BR-MTG-009 | A meeting has at most one active consensus-rescheduling process by default. | Proposed | Database uniqueness/state policy | UC-MTG-003 | +| BR-MTG-010 | Abandoning a rescheduling process does not change the original meeting. | Proposed | Transaction/state model | UC-MTG-003 | +| BR-MTG-011 | Participant and host relationships survive rescheduling unless explicitly changed. | Proposed | Domain model | UC-MTG-002, UC-MTG-003 | + +## Time and timezone semantics + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-TIME-001 | Stored user and schedule timezone values are valid IANA identifiers. | Partial | Current validation checks non-empty strings | UC-ID-001, UC-SCH-001 | +| BR-TIME-002 | Recurring local schedules retain the timezone in which they were defined. | Current | User timezone plus local window strings | UC-SCH-001 | +| BR-TIME-003 | Scheduled candidate and meeting instances are stored as absolute timestamps. | Current | PostgreSQL/Prisma `DateTime` | UC-BKG-001, UC-POL-001 | +| BR-TIME-004 | Local poll candidate input is converted using an explicit organizer timezone, not the server runtime timezone. | Proposed | Timezone-aware domain service | UC-POL-001 | +| BR-TIME-005 | Displayed times identify or clearly imply the viewer timezone and use locale-aware formatting. | Partial | Implemented in parts of booking; inconsistent elsewhere | All scheduling journeys | +| BR-TIME-006 | Daylight-saving transitions produce either a valid unambiguous instant or a user-visible validation decision. | Proposed | Timezone domain service and tests | UC-SCH-001, UC-POL-001 | + +### Time concepts must remain separate + +```text +Recurring rule: Monday at 09:00 in America/New_York +Scheduled instant: 2026-11-02T14:00:00Z +Viewer display: Monday at 19:30 in Asia/Kolkata +``` + +These values are related, but they are not interchangeable representations of the same field. + +## Notifications and provider integrations + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-NOT-001 | External notification failure does not roll back committed booking or meeting state. | Partial | Implemented for booking email on feature branch; broader lifecycle proposed | UC-BKG-001, UC-POL-003, UC-MTG-001 | +| BR-NOT-002 | Required asynchronous work is recorded durably before delivery is considered scheduled. | Proposed | Outbox/job record in business transaction | UC-POL-003, UC-MTG-001, UC-MTG-002 | +| BR-NOT-003 | A delivery attempt has an independent status, timestamps, provider reference, and safe failure detail. | Proposed | Notification model | Notification processing | +| BR-NOT-004 | Retried provider operations reuse stable internal and provider identities where required. | Proposed | Idempotency/provider adapter | UC-BKG-001, UC-MTG-002 | +| BR-NOT-005 | Secrets and invitation/management tokens never appear in logs or persisted plaintext when hashing can satisfy verification. | Proposed | Logging policy and token service | UC-POL-002, UC-MTG-001 | +| BR-NOT-006 | Provider payloads contain only the minimum personal data needed for delivery. | Proposed | Adapter contract and privacy review | All notification flows | + +## Reliability and integrity + +| ID | Rule | Status | Enforcement | Related use cases | +| --- | --- | --- | --- | --- | +| BR-REL-001 | Retried commands do not create duplicate logical outcomes. | Proposed broadly | Idempotency keys and unique constraints | Creation/finalization use cases | +| BR-REL-002 | Multi-record lifecycle transitions are atomic when partial state would be invalid. | Partial | Some nested writes/transactions exist; not governed across lifecycle | UC-POL-001, UC-POL-003, UC-MTG-003 | +| BR-REL-003 | Public mutations have rate limits and abuse controls proportional to cost and exposure. | Proposed | Request boundary/infrastructure | UC-BKG-001, UC-POL-002 | +| BR-REL-004 | Error responses do not expose secrets, provider credentials, or unnecessary internal details. | Partial | Generic errors used in several actions; no documented global policy | All use cases | +| BR-REL-005 | Durable business records use timestamps sufficient to reconstruct lifecycle order. | Partial | Created/updated timestamps exist; lifecycle-event timestamps are missing | Poll and meeting lifecycle | +| BR-REL-006 | Deletion behavior preserves required historical and audit information. | Unresolved | Current cascade rules may delete history | Event type, user, workspace deletion | + +## Rules requiring database support + +The following rules are strong candidates for database enforcement because application-only checks are vulnerable to concurrency or alternate write paths: + +| Rule | Candidate database mechanism | +| --- | --- | +| BR-ID-001 | Normalized username column or database-compatible case-insensitive uniqueness | +| BR-SCH-003 | Existing `(userId, day)` unique constraint | +| BR-EVT-001 | Existing `(userId, slug)` unique constraint; future scope may change | +| BR-BKG-003 | PostgreSQL exclusion constraint, serializable strategy, or a slot-reservation model after design evaluation | +| BR-BKG-006 | Unique idempotency key scoped to operation/owner | +| BR-POL-007 | Relationship ensuring finalized candidate belongs to poll | +| BR-POL-008 | Unique resulting meeting relationship for poll | +| BR-RES-002 | Unique `(participantId, candidateId)` | +| BR-RES-003 | Schema design that prevents cross-poll participant/candidate references | +| BR-MTG-008 | Version column used for optimistic concurrency | +| BR-MTG-009 | Conditional uniqueness or state-aware application transaction | + +The exact PostgreSQL and Prisma representation will be selected during proposed ER design. This table records the invariant, not a premature implementation choice. + +## Rules requiring application/domain enforcement + +Database constraints are not sufficient for contextual decisions such as: + +- Whether an actor has the right workspace role +- Whether an event type is currently bookable +- Whether a requested time belongs to generated availability +- Whether required participants satisfy finalization policy +- Which poll state transition is allowed +- Whether a cancellation notice window permits guest cancellation +- How timezone inconvenience contributes to recommendation ranking + +These belong in module services with focused tests. The database should still enforce the simpler structural facts those decisions depend on. + +## Rules-to-tests examples + +| Rule | Unit test | Integration/database test | End-to-end test | +| --- | --- | --- | --- | +| BR-SCH-002 | Reject `17:00–09:00` | Optional check-constraint test | Form displays validation error | +| BR-BKG-001 | Reject event/host mismatch | Persist mismatched IDs through service and verify rejection | Tampered booking request fails | +| BR-BKG-003 | Overlap calculation cases | Concurrent booking attempts produce one success | Two browser submissions cannot double-book | +| BR-RES-003 | Reject candidate from another poll | Database/service prevents cross-poll write | Tampered vote request fails | +| BR-POL-008 | Repeated finalization returns one meeting | Concurrent finalization creates one meeting | Double-click finalize is safe | +| BR-NOT-001 | Provider failure returns separate status | Booking remains stored when provider fails | Confirmation UI shows meeting success plus warning | + +Not every rule needs every test layer. Choose the lowest-cost layer that provides credible evidence, then add integration or end-to-end coverage for boundaries and concurrency. + +## Unresolved policy decisions + +These questions block parts of the proposed ER model: + +1. Are availability windows allowed to overlap, or are they normalized automatically? +2. Are event types ever physically deleted after producing bookings? +3. What exact booking-overlap strategy will be used in PostgreSQL? +4. Does `Booking` remain the scheduled commitment or become a request/source for `Meeting`? +5. Can polls be both open-link and invitation-only? +6. Can a participant intentionally leave a candidate unanswered? +7. Which host or workspace roles may finalize, cancel, and reschedule? +8. What cancellation and rescheduling notice policies apply? +9. Is history stored as revisions, domain events, audit entries, or a combination? +10. Which durable background-work mechanism fits the deployment environment? + +Unresolved rules should not be disguised as database defaults. They require explicit product and architecture decisions. + +## Change process + +When a business rule changes: + +1. Update this catalogue and its status. +2. Review affected use cases and journeys. +3. Review database constraints and migration impact. +4. Update module/service implementation. +5. Update tests that reference the rule. +6. Record an ADR if the change is long-lived, cross-cutting, or expensive to reverse. + +## Next documentation step + +Create the **current domain model and ER diagram** directly from the existing Prisma schema. + +The current-model document should identify: + +- Entities and their present names +- Primary and foreign keys +- Cardinality and optionality +- Unique constraints and indexes +- Referential actions +- Rules currently enforced by schema +- Rules that the schema cannot currently enforce +- Naming collisions such as `Availability` meaning a poll vote while `UserAvailability` means a schedule + +Only after the current model is accurate should a separate proposed ER model be designed. + diff --git a/docs/content-opportunities.md b/docs/content-opportunities.md index 5ac5097..321aa70 100644 --- a/docs/content-opportunities.md +++ b/docs/content-opportunities.md @@ -90,7 +90,7 @@ Possible formats: - **Short/Reel:** “Your feature works, but does the journey finish?” - **LinkedIn/X:** A before/after user-journey diagram -Readiness: **Ready after user journeys are documented.** +Readiness: **Ready for an outline; the current and target journeys are now documented.** ### 4. Designing timezone fairness for distributed teams @@ -152,6 +152,54 @@ Possible formats: Readiness: **Architecture decision is ready; implementation content should wait for the first modular vertical slice.** +### 8. Business rules are not validation code + +Core lesson: + +> A business invariant can require coordinated enforcement across validation, a database constraint, a transaction, and tests. + +Possible formats: + +- **Article:** From business rule to database constraint and test strategy +- **YouTube:** Audit which rules a real schema enforces and which it only appears to enforce +- **Short/Reel:** “Your Zod schema is not your domain model” +- **LinkedIn:** Compare UI validation, application policy, and database invariants +- **X:** Keep the post general—for example, explain why a pre-insert uniqueness query still needs a unique constraint + +Readiness: **Ready for an outline; implementation examples should follow the ER and constraint review.** + +### 9. Reading an ER diagram as a set of guarantees + +Core lesson: + +> An ER diagram is valuable when it explains cardinality, optionality, ownership, and integrity—not merely table boxes connected by lines. + +Possible formats: + +- **Article:** Audit an existing relational schema before redesigning it +- **YouTube:** Trace keys, cascades, normalization trade-offs, and missing invariants +- **Short/Reel:** “A foreign key proves existence, not authorization” +- **LinkedIn:** How redundant foreign keys can create valid-looking but contradictory rows +- **X:** Keep examples generic, such as explaining why two valid foreign keys can still form an invalid combination + +Readiness: **Ready for an outline; proposed-model comparison will strengthen the long-form version.** + +### 10. Separate the scheduled commitment from how it was created + +Core lesson: + +> Several workflows can produce the same business outcome without forcing their source-specific fields into one table. + +Possible formats: + +- **Article:** Model a shared aggregate produced by multiple workflows +- **YouTube:** Compare one nullable universal table with a core outcome plus source records +- **Short/Reel:** “The workflow that creates a record is not always the record itself” +- **LinkedIn:** Explain provenance versus current business state +- **X:** Keep it general: distinguish an order from the checkout session, a shipment from its purchase flow, or a meeting from its scheduling method + +Readiness: **Ready after DD-001 and DD-002 are accepted and represented in the proposed ER model.** + ## Publishing record template When an idea is selected, add: @@ -179,4 +227,6 @@ Published URLs: - Credit external sources and libraries. - Do not manufacture performance claims without measurements. - Keep portfolio content connected to the actual repository history. +- Keep X posts application-agnostic: publish the general engineering principle rather than a project update. +- Use articles and YouTube for project-backed case studies when implementation evidence improves the lesson. diff --git a/docs/domain-decisions.md b/docs/domain-decisions.md new file mode 100644 index 0000000..71f6522 --- /dev/null +++ b/docs/domain-decisions.md @@ -0,0 +1,667 @@ +# Proposed Domain Decisions + +**Document status:** Recommended for review +**Purpose:** Resolve the major choices exposed by the journeys, use cases, business rules, and current ER audit before drawing the proposed ER model + +## How to use this document + +This document is a decision workshop, not an implementation specification. + +Each decision contains: + +- The problem +- Recommended direction +- Alternatives considered +- Consequences +- Rules and use cases affected +- Questions still requiring confirmation + +Decision statuses: + +| Status | Meaning | +| --- | --- | +| **Recommended** | Preferred direction based on current evidence; awaiting explicit acceptance | +| **Accepted** | Approved direction suitable for proposed-model design | +| **Deferred** | Intentionally postponed because current use cases do not require it | +| **Unresolved** | More product or technical evidence is required | + +Accepted decisions that are expensive to reverse will later receive their own ADRs. + +## Decision summary + +| ID | Decision | Recommendation | Status | +| --- | --- | --- | --- | +| DD-001 | Scheduled commitment | Introduce `Meeting` as the shared final commitment | Recommended | +| DD-002 | Direct-booking source | Retain a source record linked one-to-one with its meeting | Recommended | +| DD-003 | Poll outcome | Finalized poll links to one selected candidate and one meeting | Recommended | +| DD-004 | Participant identity | Use poll/meeting-scoped participant entities with optional user link | Recommended | +| DD-005 | Ownership boundary | Introduce a personal workspace for every user; add team UX later | Recommended | +| DD-006 | Availability model | Separate schedule timezone from user profile and normalize windows | Recommended | +| DD-007 | Lifecycle history | Current state plus append-only lifecycle events; not full event sourcing | Recommended | +| DD-008 | Deletion and retention | Archive configuration; preserve/anonymize transactional history | Recommended | +| DD-009 | Notifications | Persist notification intent and delivery attempts separately | Recommended | +| DD-010 | External integrations | Keep provider connections/references outside core meeting columns | Recommended | +| DD-011 | Concurrency | Database-backed overlap protection plus command idempotency | Recommended, mechanism unresolved | +| DD-012 | Open and private polls | Support both through explicit access policy | Recommended | + +--- + +## DD-001: Introduce Meeting as the shared scheduled commitment + +**Status:** Recommended + +### Problem + +The current `Booking` record represents a direct guest reservation. Poll finalization also needs to create a scheduled commitment, but a poll is not naturally a booking made by one guest. + +If poll outcomes are forced into the current `Booking` shape, fields such as one guest name and one guest email become misleading. If direct bookings and poll outcomes create unrelated scheduled entities, cancellation, rescheduling, calendar synchronization, and meeting management must be implemented twice. + +### Recommendation + +Introduce `Meeting` as the aggregate root for a finalized scheduled commitment. + +Both paths converge on it: + +```text +Event type + direct booking request ──┐ + ├──> Meeting +Poll + selected candidate ───────────┘ +``` + +`Meeting` owns: + +- Scheduled start and end +- Current lifecycle status +- Host/participant relationships +- General meeting location +- Version for concurrent changes +- Creation source +- Lifecycle history +- External calendar references through integration entities + +### Alternatives considered + +#### Keep `Booking` as the universal scheduled entity + +Simpler migration, but the name and guest-specific fields fit direct booking better than group outcomes. It risks a growing collection of nullable poll/team fields. + +#### Create separate `BookingMeeting` and `PollMeeting` + +Preserves source-specific models but duplicates cancellation, rescheduling, participant, notification, and integration behavior. + +### Consequences + +- Meeting lifecycle is implemented once. +- Current bookings require migration or transitional mapping. +- “Booking” and “meeting” receive distinct meanings. +- Source-specific input remains outside the core meeting. + +### Affected rules/use cases + +- UC-BKG-001 +- UC-POL-003 +- UC-MTG-001 through UC-MTG-003 +- BR-MTG-001 through BR-MTG-011 + +--- + +## DD-002: Preserve a direct-booking source record + +**Status:** Recommended + +### Problem + +Direct booking captures facts that do not belong to every meeting: + +- Event type used +- Guest-submitted name, email, timezone, and notes +- Public booking idempotency key +- Booking-specific authorization/management token + +Discarding this information after creating a meeting loses provenance. Storing it all on `Meeting` makes poll-created meetings sparse and semantically unclear. + +### Recommendation + +Retain a direct-booking source entity linked one-to-one with the resulting meeting. + +Working term: + +```text +DirectBooking + ├── source EventType + ├── guest submission snapshot + └── resulting Meeting +``` + +The current `Booking` table may evolve into this source entity during migration rather than being deleted immediately. + +### Snapshot principle + +Store enough accepted booking context to interpret the meeting even if the event type later changes or is archived. Exact snapshot fields will be selected in the proposed ER model. + +### Consequences + +- Meeting remains source-neutral. +- Historical direct-booking context survives event-type edits. +- A one-to-one constraint prevents one booking request from creating several meetings. +- Migration can reuse current booking IDs or preserve a mapping. + +--- + +## DD-003: Finalized poll has one selected candidate and one meeting + +**Status:** Recommended + +### Problem + +The current poll has candidates and votes but no lifecycle outcome. Proposed finalization must prevent: + +- Selecting a candidate from another poll +- Creating multiple meetings from repeated finalization +- Marking the poll finalized without creating its meeting +- Creating the meeting without locking the poll + +### Recommendation + +A finalized poll records: + +- `selectedCandidateId` +- `finalizedAt` +- Finalizing actor or automation source +- `meetingId` + +The selected candidate must belong to the same poll. Poll finalization, meeting creation, participant creation, and durable external-work creation occur in one transaction. + +### Structural direction + +Prefer a relationship design that makes cross-poll candidate selection impossible or enforceable through a composite key, rather than relying only on a pre-check. + +### Consequences + +- Poll has one authoritative outcome. +- Repeated finalization can return the existing meeting. +- Poll and meeting lifecycles remain distinct but connected. +- Cancelling the meeting does not reopen the poll automatically. + +### Open policy + +Whether an authorized host may explicitly reopen a finalized poll is deferred. Default recommendation: no backward transition; create a rescheduling poll instead. + +--- + +## DD-004: Use scoped participant entities + +**Status:** Recommended + +### Problem + +The current poll repeats participant name and email on every vote. Name is simultaneously treated as display label, identity, and edit authorization. + +A global “person” table is also risky because accountless identities may be duplicated, mistyped, privacy-sensitive, or unrelated across workspaces. + +### Recommendation + +Use scoped participant entities: + +```text +PollParticipant +├── pollId +├── optional userId +├── displayName +├── normalized email when supplied +├── participant role: REQUIRED / OPTIONAL +├── response state +└── invitation/edit-token metadata + +MeetingParticipant +├── meetingId +├── optional userId +├── displayName/email snapshot +├── attendance role +└── response/delivery context when needed +``` + +Individual candidate preferences reference `PollParticipant.id`, not a participant name. + +### Why not one global Contact initially? + +- Contact deduplication rules are unclear. +- Two workspaces may intentionally have independent contact records. +- Email is not always available or immutable. +- Global contact identity introduces privacy and ownership questions not required by the first use cases. + +### Consequences + +- Same-name participants do not collide. +- Email is stored once per poll participant rather than once per vote. +- Accountless and authenticated participation coexist. +- Poll participants can be copied into meeting participants at finalization. +- Poll and meeting participant records preserve context-specific snapshots. + +### Open policy + +The exact token design, response anonymity, and email-normalization policy remain to be specified in security/system architecture. + +--- + +## DD-005: Introduce personal workspaces as the ownership boundary + +**Status:** Recommended + +### Problem + +Most current resources belong directly to `User`. Future collaboration requires shared resources, roles, invitations, and tenant isolation. Adding workspace ownership after large amounts of user-owned data exist would require a broader migration. + +Exposing organization concepts immediately, however, would complicate the experience for individual users. + +### Recommendation + +Every user receives one personal workspace. Team-workspace creation and switching remain later UI capabilities. + +```text +User + └── WorkspaceMembership (OWNER) + └── Personal Workspace +``` + +Workspace owns product resources such as: + +- Event types +- Availability schedules where appropriate +- Polls +- Meetings +- Notification policy/configuration + +User fields still identify actors such as creator, host, finalizer, or canceller. + +### Ownership distinction + +```text +workspaceId = Which tenant owns this data? +createdById = Which user created it? +hostId = Which user hosts the meeting? +``` + +These are separate questions. + +### Alternatives considered + +#### Keep direct user ownership until team features + +Simpler now, but creates a later migration across most product tables. + +#### Put one `organizationId` on User + +Rejects multi-workspace membership and cannot represent different roles in different workspaces. + +### Consequences + +- Multi-tenant boundary becomes explicit early. +- Individual UX can remain simple by hiding the workspace switcher when only one exists. +- Every resource query must include or derive workspace scope. +- Personal workspace provisioning must be idempotent. +- Migration must create one workspace and owner membership per existing user. + +### Open policy + +- Whether availability is user-owned but workspace-visible, or workspace-owned and assigned to a user +- Resource transfer when a member leaves +- Workspace deletion and retention + +--- + +## DD-006: Normalize recurring availability windows + +**Status:** Recommended + +### Problem + +Current `UserAvailability` stores summary `startTime`/`endTime` plus JSON `slots`. This duplicates information and prevents strong database relationships or window-level queries. + +The user profile timezone also acts as the interpretation timezone for all recurring schedules, coupling profile preferences to scheduling rules. + +### Recommendation + +Introduce an availability-schedule root with its own timezone and child windows. + +```text +AvailabilitySchedule +├── owner/host context +├── workspace scope +├── timezone +├── active/default metadata +└── AvailabilityWindow[] + ├── weekday + ├── local start + └── local end +``` + +Disabled days can be represented by absence of windows unless later UX requires an explicit disabled-day record. + +### Why normalize here? + +Expected requirements include: + +- Multiple windows per day +- Window-level validation +- Overlap detection +- Candidate generation +- Possibly multiple schedules or overrides later +- Clear single source of truth + +These provide stronger evidence for child rows than generic normalization preference alone. + +### Consequences + +- JSON migration is required. +- Window constraints become clearer. +- Full schedule replacement still requires one transaction. +- Profile timezone can change without silently reinterpreting an existing schedule. + +### Deferred detail + +The physical representation of local time—database `time`, minute-of-day integer, or validated string—will be chosen after reviewing Prisma/PostgreSQL behavior and required calculations. + +--- + +## DD-007: Current state plus append-only lifecycle events + +**Status:** Recommended + +### Problem + +A mutable `status` field answers what is true now but cannot explain: + +- Who changed it +- Why it changed +- Previous scheduled time +- Whether automation or a person initiated it +- Which notification or provider work followed + +Full event sourcing would preserve every change but introduces substantial complexity not justified by current requirements. + +### Recommendation + +Use: + +1. Current state on the aggregate for efficient reads. +2. Append-only lifecycle events for important transitions. +3. A version number for optimistic concurrency. + +Example: + +```text +Meeting +├── status = SCHEDULED +├── startAt / endAt +└── version = 3 + +MeetingEvent +├── type = RESCHEDULED +├── actor +├── occurredAt +└── structured transition context +``` + +This is an audit/history pattern, not full event sourcing. Current state remains authoritative and is not rebuilt from every event on normal reads. + +### Consequences + +- Common reads remain simple. +- Important transitions are explainable. +- Rescheduling history can be retained. +- Event payload design and privacy retention require discipline. +- Domain transition service must update current state and append event atomically. + +--- + +## DD-008: Archive configuration and preserve transactional history + +**Status:** Recommended + +### Problem + +Current cascades can delete bookings when event types or users are deleted. Configuration and historical commitments have different retention needs. + +### Recommendation + +- Archive event types after they have produced scheduled history. +- Prevent routine hard deletion of referenced configuration. +- Store accepted snapshots required to interpret historical meetings. +- Preserve meeting lifecycle history subject to privacy policy. +- Anonymize or detach personal data where deletion obligations require it rather than blindly cascading every transactional record. +- Reserve hard deletion for controlled retention/privacy workflows. + +### Consequences + +- Referential actions become more conservative. +- Account deletion requires an explicit workflow. +- Historical analytics and audit remain possible. +- Privacy rules must define which participant fields can remain and for how long. + +### Unresolved policy + +Formal data-retention periods and legal/privacy obligations are outside the current portfolio scope but must be decided before real external usage. + +--- + +## DD-009: Persist notification intent and attempts separately + +**Status:** Recommended + +### Problem + +External delivery can fail after business state commits. A single status on `Meeting` cannot represent multiple recipients, channels, retries, or provider attempts. + +### Recommendation + +Separate notification intent from delivery attempts: + +```text +Notification +├── business event and recipient +├── channel/template +├── current delivery state +└── NotificationAttempt[] + ├── attemptedAt + ├── provider + ├── provider reference + └── sanitized outcome +``` + +Notification intent is created in the same transaction as the business transition when delivery is required. A background processor performs attempts after commit. + +### Consequences + +- Meeting and email truth remain separate. +- Retries and delivery history become visible. +- One meeting can notify several participants independently. +- A job-claiming and retry policy is required. +- Template input must be reproducible without relying on mutable UI state. + +### Deferred detail + +Whether `Notification` itself acts as an outbox job or references a generic job table will be decided in system architecture. + +--- + +## DD-010: Isolate provider-specific integration data + +**Status:** Recommended + +### Problem + +The current `googleMeetLink` field couples a core booking record to one provider. Calendar and conferencing providers have different event IDs, account connections, synchronization states, and webhook versions. + +### Recommendation + +Core `Meeting` stores provider-neutral location meaning. Provider-specific state lives in integration entities: + +```text +CalendarConnection +├── user/workspace ownership +├── provider +├── external account reference +├── encrypted credential reference +└── connection status + +ExternalCalendarEvent +├── meetingId +├── connectionId +├── provider event ID +├── calendar ID +├── sync status/version +└── last synchronized time +``` + +Meeting location may use provider-neutral types such as video, phone, in-person, or custom URL, with a separate integration reference when generated externally. + +### Consequences + +- Core meeting model does not change for every provider. +- Provider retries and webhook reconciliation have a home. +- Credential storage requires a security design separate from ordinary product data. +- One meeting can potentially synchronize to more than one calendar without repeated core columns. + +--- + +## DD-011: Use database-backed overlap protection and command idempotency + +**Status:** Recommended; exact mechanism unresolved + +### Problem + +An application query followed by insert cannot prevent concurrent overlapping reservations. Network retries can also repeat a logically identical command. + +These are different problems: + +- Concurrency invariant: two active intervals must not overlap. +- Idempotency invariant: one logical command must not create two outcomes. + +### Recommendation + +Use both: + +1. Database-backed overlap protection for blocking meeting/reservation intervals. +2. Scoped unique idempotency keys for public creation/finalization commands. + +### Mechanisms to evaluate + +- PostgreSQL range/exclusion constraint with status-aware behavior +- Serializable transaction or targeted advisory locking +- Explicit slot/hold reservation model when candidate granularity permits it + +The chosen approach must support variable-length intervals, cancellation semantics, and Prisma migrations without hiding critical SQL behavior. + +### Consequences + +- Friendly pre-check remains useful but is not final enforcement. +- Database/provider errors require domain-safe translation. +- Concurrency integration tests are required. +- Custom SQL migration may be justified and should be documented if selected. + +### ADR threshold + +The exact overlap mechanism is expensive to reverse and should receive an ADR after a focused database design experiment. + +--- + +## DD-012: Support open-link and invitation-only polls explicitly + +**Status:** Recommended + +### Problem + +Open polls reduce friction but provide weak identity and abuse protection. Invitation-only polls support controlled participation but require participant and token lifecycle management. + +### Recommendation + +Model poll access policy explicitly rather than inferring it from whether invitation rows exist. + +Initial policy candidates: + +```text +OPEN_LINK +INVITATION_ONLY +``` + +Possible future policies such as workspace-only should be added only with a real use case. + +### Consequences + +- Query and mutation authorization can branch on one explicit policy. +- Invitation-only participants have durable identity and edit access. +- Open-link participants still need rate limiting and a private edit mechanism. +- Result visibility should be a separate policy rather than implied by access mode. + +--- + +## Proposed aggregate map + +This is a conceptual map, not yet the proposed ER diagram. + +```mermaid +flowchart LR + Identity[Identity module] + Workspace[Workspace aggregate] + Schedule[Availability Schedule aggregate] + EventType[Event Type aggregate] + Poll[Poll aggregate] + Meeting[Meeting aggregate] + Notification[Notification aggregate] + Integrations[Integration references] + + Identity --> Workspace + Workspace --> Schedule + Workspace --> EventType + Workspace --> Poll + Workspace --> Meeting + EventType -->|direct booking source| Meeting + Poll -->|finalization| Meeting + Meeting --> Notification + Meeting --> Integrations +``` + +Aggregate boundaries indicate transactional ownership, not separate services or databases. + +## Decisions likely to become ADRs + +After review, create ADRs for: + +1. Unified `Meeting` aggregate for direct and poll scheduling +2. Personal-workspace tenancy boundary +3. Meeting lifecycle state plus append-only history +4. Database overlap-protection strategy +5. Durable notification/outbox approach + +Participant and schedule representation may remain domain-model decisions unless their trade-offs become cross-cutting or difficult to reverse. + +## Review checklist + +Before accepting these recommendations, verify: + +- [ ] Direct booking still has a simple guest experience. +- [ ] Poll finalization creates exactly one meeting. +- [ ] Accountless participation remains possible. +- [ ] Workspace concepts do not unnecessarily appear for single users. +- [ ] Schedule timezone does not silently change with profile preferences. +- [ ] Historical meetings survive configuration changes. +- [ ] Deletion and privacy are intentional. +- [ ] Provider failures remain separate from meeting truth. +- [ ] Concurrency and idempotency solve different invariants. +- [ ] Migration from every current table is explainable. + +## Next documentation step + +After reviewing or accepting these decisions, create `domain-model-proposed.md` with: + +- Entity definitions +- ER diagram +- Keys and cardinalities +- Lifecycle enums and transition diagrams +- Unique and check constraints +- Aggregate transaction boundaries +- Index/access-pattern analysis +- Mapping from current tables to proposed tables +- Incremental migration phases + diff --git a/docs/domain-model-current.md b/docs/domain-model-current.md new file mode 100644 index 0000000..b73a317 --- /dev/null +++ b/docs/domain-model-current.md @@ -0,0 +1,806 @@ +# Current Domain and ER Model + +**Document status:** Current-state analysis +**Schema source:** `packages/db/prisma/schema.prisma` on the documentation branch +**Reviewed:** 2026-08-29 + +## Purpose + +This document describes the database model that exists today. It does not add proposed entities or silently correct current limitations. + +Its goals are to: + +- Make current entities and relationships understandable. +- Record keys, constraints, indexes, nullability, and referential actions. +- Explain which business rules the database enforces. +- Identify integrity gaps and ambiguous ownership. +- Provide a factual baseline for a separate proposed ER model. + +## ER concepts used in this document + +### Entity + +An entity is a distinguishable domain or infrastructure concept whose instances are stored independently. In the current schema, `User`, `EventType`, `Booking`, and `Poll` are entities. + +An entity is not simply every noun visible in the UI. “Host” and “guest,” for example, are roles. The current schema stores a host as a `User`, while guest details are embedded in `Booking`. + +### Attribute + +An attribute is a stored property of an entity, such as `Booking.startTime` or `EventType.duration`. + +Attributes may be: + +- Required or optional +- Unique or repeatable +- Mutable or effectively immutable +- Domain values or infrastructure metadata + +### Primary key + +A primary key uniquely identifies one row. Every current model uses a surrogate string key named `id`, except `VerificationToken`, which uses a composite unique constraint but has no explicit `@id` field. + +### Foreign key + +A foreign key connects one row to another entity and preserves referential integrity. `Booking.hostId`, for example, must reference an existing `User.id`. + +Foreign keys prove that a referenced row exists. They do not automatically prove that the current actor is authorized to access it. + +### Cardinality + +Cardinality describes how many records can participate in a relationship: + +- One-to-one +- One-to-many +- Many-to-many through an associative entity + +For example, one `Poll` has many `TimeSlot` records, while each `TimeSlot` belongs to exactly one `Poll`. + +### Optionality + +Optionality describes whether a relationship or value is required. `Availability.userId` is optional, so a vote may be associated with an authenticated user or remain accountless. + +### Candidate key and unique constraint + +A candidate key is an alternate set of attributes capable of identifying a record. The current event-type scope uses `(userId, slug)` as a candidate key enforced through a composite unique constraint. + +### Index + +An index accelerates selected query patterns. It does not necessarily enforce a business invariant. A unique index/constraint can do both, but a normal index such as `Booking(hostId)` only improves lookup performance. + +### Referential action + +A referential action decides what happens to dependent rows when the referenced row is deleted: + +- `Cascade`: delete dependents automatically +- `SetNull`: retain dependent rows and clear the optional reference +- `Restrict`/`NoAction`: prevent or defer deletion when dependents exist + +Referential actions are product data-retention decisions, not merely ORM configuration. + +## Current high-level ER diagram + +```mermaid +erDiagram + USER ||--o{ ACCOUNT : has + USER ||--o{ SESSION : has + USER ||--o{ EVENT_TYPE : owns + USER ||--o{ USER_AVAILABILITY : configures + USER ||--o{ BOOKING : hosts + USER ||--o{ POLL : creates + USER o|--o{ AVAILABILITY : optionally_submits + + EVENT_TYPE ||--o{ BOOKING : classifies + + POLL ||--o{ TIME_SLOT : proposes + POLL ||--o{ AVAILABILITY : receives + TIME_SLOT ||--o{ AVAILABILITY : receives +``` + +`VerificationToken` is intentionally not connected in the diagram because the current schema stores its identifier as a string rather than a foreign key to `User`. + +## Current model groups + +```text +Identity infrastructure +├── User +├── Account +├── Session +└── VerificationToken + +Direct scheduling +├── EventType +├── UserAvailability +└── Booking + +Group polling +├── Poll +├── TimeSlot +└── Availability +``` + +These groups reflect current schema organization. They are not yet enforced modular-monolith boundaries. + +--- + +## Entity: User + +### Purpose + +Represents an authenticated SlotSyncro account and currently acts as the ownership root for most product data. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key, generated CUID | Internal user identity | +| `name` | No | None | Display name from provider/profile | +| `email` | No | Unique when present | Account email | +| `emailVerified` | No | None | Verification timestamp | +| `image` | No | None | Profile image URL | +| `username` | No | Unique when present | Public scheduling identity | +| `timeZone` | Yes | Defaults to `UTC` | Current scheduling timezone string | +| `createdAt` | Yes | Defaults to current time | Creation metadata | +| `updatedAt` | Yes | Automatically updated | Modification metadata | + +### Relationships + +| Relationship | Cardinality | Delete behavior | +| --- | --- | --- | +| User to Account | One-to-many | Deleting user cascades accounts | +| User to Session | One-to-many | Deleting user cascades sessions | +| User to EventType | One-to-many | Deleting user cascades event types | +| User to UserAvailability | One-to-many | Deleting user cascades schedules | +| User to Booking as host | One-to-many | Deleting user cascades hosted bookings | +| User to Poll | One-to-many | Deleting user cascades polls | +| User to Availability vote | One-to-many, optional from vote side | Deleting user sets vote `userId` to null | + +### Current observations + +- `User` is both identity and product-ownership root. +- Optional unique email and username allow accounts without those values. +- `timeZone` is a non-empty-required database string, but the database cannot determine whether it is a valid IANA timezone. +- `@@index([username])` may be redundant because a unique constraint already creates an index in PostgreSQL. Confirm generated migration behavior before removing it. +- Deleting a user triggers deletion of most owned product history, including hosted bookings and polls. + +### Business rules enforced + +- BR-ID-003 through `Account`, not directly on `User` +- BR-ID-004: email uniqueness when present +- Part of BR-ID-001: username uniqueness, but not normalization policy + +--- + +## Entity: Account + +### Purpose + +Stores an Auth.js external authentication account linked to one `User`. + +### Keys and constraints + +| Key | Definition | Purpose | +| --- | --- | --- | +| Primary key | `id` | Internal account record identity | +| Foreign key | `userId -> User.id` | Account owner | +| Composite unique | `(provider, providerAccountId)` | Prevent duplicate provider identity | + +### Relationship + +Many `Account` records may belong to one `User`, allowing a user to link multiple authentication providers conceptually. + +### Current observations + +- Provider access and refresh tokens are stored directly in the record because Auth.js requires provider credentials. +- Security depends on database access controls and secret handling; these fields must never be exposed through general user queries or logs. +- Deleting the user cascades account deletion. + +--- + +## Entity: Session + +### Purpose + +Provides the Auth.js adapter-compatible database-session model associated with one user. The current application configures `session.strategy` as `jwt`, so this table exists in the schema but is not the authoritative store for the active runtime session strategy. + +### Keys and constraints + +| Key | Definition | +| --- | --- | +| Primary key | `id` | +| Unique | `sessionToken` | +| Foreign key | `userId -> User.id` | + +### Current observations + +- The relationship permits multiple session rows per user when database-session behavior is used. +- Expiration is stored as a required timestamp. +- Deleting the user cascades sessions. +- The current JWT strategy stores active session state differently; schema presence does not prove runtime usage. +- Expired database-session cleanup, if that strategy is used, is an operational concern rather than an ER relationship. + +--- + +## Entity: VerificationToken + +### Purpose + +Stores a time-limited Auth.js verification token identified by an external string such as an email address. + +### Keys and constraints + +| Constraint | Definition | +| --- | --- | +| Unique token | `token` | +| Composite unique | `(identifier, token)` | + +### Current observations + +- There is no foreign key to `User`; verification may occur before a user exists. +- `identifier` is an authentication identifier, not a guaranteed user ID. +- The schema has no explicit `@id`. Prisma supports models with a unique criterion, but the model's identity semantics differ from other tables. + +--- + +## Entity: EventType + +### Purpose + +Defines a reusable direct-booking configuration owned by one user. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key | Event-type identity | +| `title` | Yes | Application length validation | Public title | +| `slug` | Yes | Unique with `userId` | Public route segment | +| `description` | No | Database text | Public context | +| `duration` | Yes | Application range validation | Meeting length in minutes | +| `isArchived` | Yes | Defaults false | Whether new bookings are disabled | +| `bufferBefore` | Yes | Defaults zero | Pre-meeting blocked minutes | +| `bufferAfter` | Yes | Defaults zero | Post-meeting blocked minutes | +| `userId` | Yes | Foreign key | Owner | + +### Keys and indexes + +| Type | Definition | Effect | +| --- | --- | --- | +| Primary key | `id` | Identifies record | +| Composite unique | `(userId, slug)` | Enforces owner-scoped public slug | +| Index | `userId` | Supports owner listing/filtering | + +### Relationships + +- Each `EventType` belongs to exactly one `User`. +- One `EventType` may have many `Booking` records. +- Deleting the owner cascades the event type. +- Deleting the event type cascades its bookings through the booking foreign key. + +### Current observations + +- Duration and buffer validity are enforced by application validation, not database checks. +- Archive state protects public creation without erasing the event type. +- Hard deletion may erase booking history because of cascading relationships. +- Booking stores the event-type relationship but does not snapshot title, duration, or buffers. Historical interpretation therefore depends on a mutable event type remaining available. + +### Business rules enforced + +- BR-EVT-001: owner-scoped slug uniqueness +- Referential part of BR-BKG-001: referenced event type and host exist, though their ownership match requires application logic + +--- + +## Entity: UserAvailability + +### Purpose + +Stores one user's recurring availability configuration for one weekday. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key | Record identity | +| `day` | Yes | `DayOfWeek` enum | Weekday | +| `isAvailable` | Yes | Defaults true | Whether the day contributes candidates | +| `startTime` | Yes | String | Legacy/summary first start time | +| `endTime` | Yes | String | Legacy/summary last end time | +| `slots` | Yes | JSON | Array of local start/end windows | +| `userId` | Yes | Foreign key | Schedule owner | + +### Keys and indexes + +| Type | Definition | +| --- | --- | +| Primary key | `id` | +| Composite unique | `(userId, day)` | +| Index | `userId` | + +### Current representation + +One row contains both scalar summary values and a JSON collection: + +```json +{ + "day": "MONDAY", + "startTime": "09:00", + "endTime": "17:00", + "slots": [ + { "startTime": "09:00", "endTime": "12:00" }, + { "startTime": "14:00", "endTime": "17:00" } + ] +} +``` + +### Current observations + +- `(userId, day)` correctly models at most one weekday configuration per user. +- Multiple windows are embedded in JSON rather than normalized into a child table. +- `startTime` and `endTime` duplicate information derivable from `slots`. +- The database cannot validate JSON window shape, ordering, overlap, or `start < end` through the Prisma schema. +- A disabled day still stores fallback windows; `isAvailable` determines whether they are effective. +- User timezone is stored on `User`, so changing it changes the interpretation of all recurring windows. + +### Normalization trade-off + +Embedding windows in JSON reduces table count and makes saving one day simple. It also moves integrity and querying responsibility into application code. + +A normalized alternative would use a `ScheduleWindow` child entity, but that is a proposed-model decision—not an automatic improvement. The correct choice depends on validation, query, mutation, and history requirements. + +### Business rules enforced + +- BR-SCH-003: one row per user and weekday +- Referential ownership to `User` + +Rules such as BR-SCH-002 and BR-SCH-006 are not enforced structurally. + +--- + +## Entity: Booking + +### Purpose + +Represents a guest reservation for one host and one event type. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key | Booking identity | +| `hostId` | Yes | Foreign key | Hosting user | +| `eventTypeId` | Yes | Foreign key | Booking configuration | +| `guestName` | Yes | Application limits | Guest label | +| `guestEmail` | Yes | Application email validation | Guest contact | +| `guestNotes` | No | Database text | Optional context | +| `guestTimeZone` | Yes | String | Guest timezone label/context | +| `startTime` | Yes | Timestamp | Scheduled absolute start | +| `endTime` | Yes | Timestamp | Scheduled absolute end | +| `status` | Yes | Defaults `ACCEPTED` | `PENDING`, `ACCEPTED`, or `CANCELLED` | +| `googleMeetLink` | No | None | Provider-specific meeting URL | + +### Keys and indexes + +| Type | Definition | Intended query support | +| --- | --- | --- | +| Primary key | `id` | Direct lookup | +| Index | `hostId` | Host booking list/conflicts | +| Index | `eventTypeId` | Event-type booking lookup | +| Composite index | `(startTime, endTime)` | Time-range filtering | + +### Relationships + +- Each booking references exactly one host `User`. +- Each booking references exactly one `EventType`. +- One host can have many bookings. +- One event type can have many bookings. +- Deleting host or event type cascades booking deletion. + +### Integrity gap: host and event type + +The database proves: + +```text +Booking.hostId references an existing User +Booking.eventTypeId references an existing EventType +``` + +It does not prove: + +```text +Booking.eventType.userId == Booking.hostId +``` + +The Server Action currently enforces that comparison. Alternate write paths could violate it unless they reuse the same domain operation. + +### Integrity gap: overlapping intervals + +Indexes improve finding conflicts but do not prevent them. No current database constraint prevents two accepted bookings for the same host from overlapping. + +The existing indexes also do not exactly match the main conflict predicate, which combines host, status, start, and end. Query plans should be measured before proposing a replacement index. + +### Current observations + +- `status` is an enum but permitted transitions are not modeled. +- `endTime > startTime` is not a database constraint. +- `googleMeetLink` couples the booking model to one conferencing provider. +- Guest details are embedded because a guest entity does not exist. +- No idempotency key protects repeated logical submissions. +- No version field protects concurrent lifecycle changes. +- On the documented schema baseline, no persistent notification status or stable ICS UID exists. The email-invitation feature branch introduces related changes and must update this model when merged. + +### Business rules enforced + +- Required host and event-type existence +- Status limited to `BookingStatus` +- Basic timestamp storage + +Overlap, valid generated-slot membership, timezone semantics, and lifecycle transitions remain application/domain concerns or gaps. + +--- + +## Entity: Poll + +### Purpose + +Represents a host-created group availability question with public slug and candidate time slots. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key | Poll identity | +| `title` | Yes | Application length validation | Poll title | +| `description` | No | None | Poll context | +| `slug` | Yes | Globally unique | Public route identity | +| `hostId` | Yes | Foreign key | Poll creator/owner | + +### Keys and indexes + +| Type | Definition | +| --- | --- | +| Primary key | `id` | +| Unique | `slug` | + +There is no explicit index on `hostId` in the current schema. PostgreSQL does not automatically index foreign-key columns merely because they are foreign keys. + +### Relationships + +- Poll belongs to exactly one host `User`. +- Poll owns many `TimeSlot` candidates. +- Poll has many `Availability` vote rows. +- Deleting poll cascades candidates and vote rows. +- Deleting host cascades polls. + +### Current observations + +- Poll has no lifecycle status, deadline, duration, organizer timezone, selected candidate, or resulting meeting relationship. +- Public slug is global rather than owner-scoped. +- Host ownership exists, but current public poll page has no host-management controls. +- `responses` points to individual vote rows, not one response entity per participant. + +--- + +## Entity: TimeSlot + +### Purpose + +Represents one candidate interval proposed by a poll. + +### Attributes and relationships + +| Attribute | Required? | Constraint | +| --- | --- | --- | +| `id` | Yes | Primary key | +| `pollId` | Yes | Foreign key to Poll | +| `startTime` | Yes | Timestamp | +| `endTime` | Yes | Timestamp | + +- Each time slot belongs to exactly one poll. +- Each time slot receives many `Availability` vote rows. +- Deleting poll cascades time slots. +- Deleting a time slot cascades its votes. + +### Current observations + +- Name `TimeSlot` is generic even though it currently means poll candidate. +- There is no uniqueness constraint preventing duplicate intervals within one poll. +- There is no check ensuring `endTime > startTime`. +- There is no explicit index on `pollId`. +- Timezone context is not stored on poll or candidate; timestamps are constructed using runtime interpretation before persistence. + +--- + +## Entity: Availability + +### Purpose + +Despite its generic name, `Availability` currently represents one participant's preference for one poll candidate. + +It acts as an associative entity connecting participation to a `TimeSlot`, but participant identity is embedded rather than normalized. + +### Important attributes + +| Attribute | Required? | Constraint/default | Meaning | +| --- | --- | --- | --- | +| `id` | Yes | Primary key | Vote-row identity | +| `pollId` | Yes | Foreign key | Declared poll | +| `timeSlotId` | Yes | Foreign key | Candidate being rated | +| `participantName` | Yes | Part of composite unique | Display name and current replacement identity | +| `participantEmail` | No | None | Repeated optional contact | +| `userId` | No | Foreign key with `SetNull` | Optional authenticated identity | +| `status` | Yes | Defaults `YES` | `YES`, `IF_NEEDED`, or `NO` | + +### Keys and constraints + +| Type | Definition | Effect | +| --- | --- | --- | +| Primary key | `id` | Vote-row identity | +| Composite unique | `(timeSlotId, participantName)` | Prevents same exact stored name voting twice on one candidate | +| Foreign key | `pollId -> Poll.id` | Poll must exist | +| Foreign key | `timeSlotId -> TimeSlot.id` | Candidate must exist | +| Optional foreign key | `userId -> User.id` | User may exist; deletion sets null | + +### Many-to-many interpretation + +Conceptually, participants and candidates form a many-to-many relationship: + +```text +One participant expresses preferences for many candidates. +One candidate receives preferences from many participants. +``` + +The current schema has no `PollParticipant` entity, so each vote repeats participant name and email. + +### Integrity gap: cross-poll mismatch + +The database permits this shape: + +```text +Availability.pollId -> Poll A +Availability.timeSlotId -> TimeSlot belonging to Poll B +``` + +Both foreign keys are individually valid. Nothing enforces that the time slot belongs to the same poll recorded on the vote. + +This is a classic example of redundant relationship data creating an integrity dependency. + +### Identity and update anomalies + +- `participantName` is treated as identity even though names are not stable or unique. +- Application deletion is case-insensitive, while database uniqueness follows database collation/operator semantics for the stored string. +- Participant email is repeated once per candidate and can become inconsistent within one person's response. +- Changing a participant name can appear to create a new participant. +- Two different people with the same name can overwrite or conflict with one another. +- Default `YES` makes unanswered and affirmative response difficult to distinguish at the UI/domain boundary. + +### Delete behavior + +- Deleting poll cascades the vote. +- Deleting time slot cascades the vote. +- Deleting optional authenticated user retains the vote and sets `userId` to null. + +Retaining an accountless historical vote after user deletion is conceptually reasonable, but privacy and display-name retention policy still require design. + +--- + +## Current cardinality summary + +| Parent | Child | Relationship | Child requires parent? | +| --- | --- | --- | --- | +| User | Account | One-to-many | Yes | +| User | Session | One-to-many | Yes | +| User | EventType | One-to-many | Yes | +| User | UserAvailability | One-to-many | Yes | +| User | Booking as host | One-to-many | Yes | +| User | Poll | One-to-many | Yes | +| User | Availability vote | One-to-many | No; optional user | +| EventType | Booking | One-to-many | Yes | +| Poll | TimeSlot | One-to-many | Yes | +| Poll | Availability vote | One-to-many | Yes | +| TimeSlot | Availability vote | One-to-many | Yes | + +No true one-to-one relationship is currently defined. + +## Current uniqueness and candidate keys + +| Model | Constraint | Domain meaning | +| --- | --- | --- | +| User | `email` | One account per non-null email | +| User | `username` | One account per non-null public username | +| Account | `(provider, providerAccountId)` | One provider identity link | +| Session | `sessionToken` | One session per token | +| VerificationToken | `token` | One record per token | +| VerificationToken | `(identifier, token)` | Verification pair uniqueness | +| EventType | `(userId, slug)` | One slug per user | +| UserAvailability | `(userId, day)` | One weekday configuration per user | +| Poll | `slug` | One public poll slug globally | +| Availability | `(timeSlotId, participantName)` | One exact-name vote per candidate | + +The final row is not a reliable participant identity rule despite being a valid database uniqueness constraint. + +## Current index inventory + +| Model | Explicit index | Likely query purpose | +| --- | --- | --- | +| User | `username` | Public owner lookup; potentially redundant with unique constraint | +| EventType | `userId` | List event types by owner | +| UserAvailability | `userId` | Load weekly schedule by user | +| Booking | `hostId` | Host booking lookup | +| Booking | `eventTypeId` | Event-type booking lookup | +| Booking | `(startTime, endTime)` | Time-range filtering | + +Potentially important foreign-key/query columns without explicit indexes include: + +- `Poll.hostId` +- `TimeSlot.pollId` +- `Availability.pollId` +- `Availability.timeSlotId` beyond its composite unique prefix +- `Availability.userId` + +An index should be justified by measured or expected access patterns. Adding every possible index increases write and storage cost. + +## Current referential actions + +```mermaid +flowchart TD + DeleteUser[Delete User] + DeleteUser -->|Cascade| Accounts + DeleteUser -->|Cascade| Sessions + DeleteUser -->|Cascade| EventTypes + DeleteUser -->|Cascade| UserSchedules + DeleteUser -->|Cascade| HostedBookings + DeleteUser -->|Cascade| Polls + DeleteUser -->|SetNull| AuthenticatedVoteLinks + + DeleteEvent[Delete EventType] -->|Cascade| EventBookings + DeletePoll[Delete Poll] -->|Cascade| Candidates + DeletePoll -->|Cascade| PollVotes + DeleteCandidate[Delete TimeSlot] -->|Cascade| CandidateVotes +``` + +### Retention concerns + +- Deleting a user removes hosted booking history. +- Deleting an event type removes its bookings. +- Deleting a poll removes all evidence of its decision process. + +These may be acceptable development defaults, but they are not neutral. The proposed model needs explicit retention, privacy, and deletion rules. + +## Current aggregate interpretation + +An aggregate is a consistency boundary: a group of entities changed through an owning root so that its invariants remain valid. + +The current schema suggests, but does not fully enforce, these possible aggregates: + +### Poll aggregate + +```text +Poll +├── TimeSlot +└── Availability votes +``` + +Nested poll creation supports this interpretation. However, direct access to `Availability` and duplicated `pollId`/`timeSlotId` relationships leave aggregate integrity dependent on application discipline. + +### Event-type configuration + +```text +EventType +└── Booking +``` + +The cascade suggests bookings are dependent children, but product semantics suggest a completed booking may need to outlive the configuration that produced it. This indicates the current referential design may not match the long-term aggregate boundary. + +### User ownership root + +`User` currently owns nearly everything. Identity deletion therefore has extensive product-data consequences. A future workspace or meeting model may redistribute ownership and retention responsibilities. + +## Normalization analysis + +### Repeating participant data + +Participant name and email repeat on every candidate vote. This creates: + +- Update anomaly: changing email requires updating many rows. +- Inconsistency risk: one response can contain different emails across candidates. +- Identity ambiguity: name acts as a key despite not being stable. + +A separate participant/response concept is a likely proposed-model improvement. + +### JSON schedule windows + +Multiple schedule windows are stored in JSON. This is intentional denormalization with trade-offs: + +- Simple retrieval and replacement of one day +- Flexible representation +- Harder database validation and querying +- Duplicated summary columns + +The proposed model must evaluate access patterns before normalizing. + +### Redundant poll relationship + +`Availability` stores `pollId` even though its `timeSlotId` already reaches a poll. Redundancy can improve query convenience but introduces the cross-poll consistency requirement that is currently unenforced. + +## Naming collisions and vocabulary debt + +| Current name | Ambiguity | Candidate future term | +| --- | --- | --- | +| `UserAvailability` | Recurring schedule, not a single availability decision | `AvailabilitySchedule` or `WeeklySchedule` | +| `Availability` | Actually a poll candidate preference/vote | `PollVote` or `CandidatePreference` | +| `TimeSlot` | Generic name currently scoped to poll candidates | `PollCandidate` | +| `responses` relation | Contains vote rows, not participant response aggregates | `votes` until response entity exists | +| `Booking` | Could mean reservation request or final meeting | Requires domain decision | +| `googleMeetLink` | Couples core record to one provider | General location/provider reference | + +Renaming should follow accepted domain decisions and migrations, not happen only for stylistic consistency. + +## Business rules currently enforced by the schema + +The database currently provides credible structural enforcement for: + +- Provider account uniqueness +- Session-token uniqueness +- User email and username uniqueness when present +- Owner-scoped event-type slug uniqueness +- One weekly schedule row per user/day +- Required host and event-type existence for booking +- Global poll-slug uniqueness +- Candidate ownership by one poll +- Supported enum values +- One exact participant-name vote per candidate +- Cascading or nullifying behavior on deletion + +## Business rules not currently enforced by the schema + +The database does not currently guarantee: + +- Valid IANA timezone strings +- Availability `start < end` +- Non-overlapping availability windows +- Atomic full-week schedule replacement +- Positive event duration/buffers when writes bypass application validation +- Booking event type belongs to booking host +- Booking interval has `end > start` +- Booking time belongs to generated availability +- No overlapping active bookings under concurrency +- Booking idempotency +- Poll lifecycle state +- Candidate interval has `end > start` +- Candidate uniqueness within poll +- Vote candidate belongs to vote poll +- Stable participant identity +- Unanswered preference differs from `NO` +- Poll finalizes once into one meeting +- Notification delivery state +- Meeting cancellation/rescheduling history + +Some of these should remain application rules; others need database support in the proposed model. + +## Questions for the proposed model + +1. Should `Booking` remain the final scheduled entity, or should direct booking create a `Meeting`? +2. Should historical meetings survive deletion of users and event-type configuration? +3. Should recurring schedule windows remain JSON or become child rows? +4. How should participant identity support authenticated and accountless people? +5. Should a poll response be a distinct entity above individual candidate preferences? +6. Can the schema structurally prevent cross-poll preference references? +7. How should one poll reference its selected candidate without allowing a candidate from another poll? +8. Which lifecycle transitions require audit records? +9. How should workspace ownership coexist with host identity? +10. Which external provider fields belong in core tables versus integration-reference tables? + +## Next documentation step + +Create `domain-model-proposed.md` only after reviewing the questions above. + +The proposed model should: + +- Name aggregate roots explicitly. +- Separate identity, ownership, participation, and authorization. +- Make invalid cross-poll relationships structurally difficult or impossible. +- Define direct-booking and poll-finalization convergence. +- Represent lifecycle and history deliberately. +- Preserve internal truth independently from external provider state. +- Identify database constraints, application rules, and transaction boundaries. +- Include a migration path from every current entity rather than presenting a clean-slate schema only. diff --git a/docs/use-cases.md b/docs/use-cases.md new file mode 100644 index 0000000..d7c88e4 --- /dev/null +++ b/docs/use-cases.md @@ -0,0 +1,991 @@ +# Use Cases + +**Document status:** Initial draft +**Scope:** Core current and proposed scheduling operations + +## Purpose + +A use case describes how an actor and the system interact to produce one meaningful result. It is more precise than a user journey but intentionally independent of UI component names, database table names, and framework APIs. + +This document is used to derive: + +- Business rules +- Authorization policies +- Domain entities and relationships +- Transaction boundaries +- Idempotency requirements +- Failure and recovery behavior +- Acceptance and end-to-end tests + +## Use case versus user journey + +```text +User journey + "A group finds and commits to a meeting time" + +Use cases within that journey + Create poll + Submit response + Revise response + Finalize poll + Create meeting + Notify participants +``` + +A journey may contain several use cases. A use case should have one primary goal and a clearly observable result. + +## Status labels + +| Status | Meaning | +| --- | --- | +| **Current** | The primary success flow exists in the repository. | +| **Partial** | Some behavior exists, but documented rules or recovery paths are missing. | +| **Proposed** | Target behavior that has not been implemented. | + +## Use-case template + +Each use case follows this structure: + +| Section | Purpose | +| --- | --- | +| Goal | Outcome the actor wants | +| Trigger | Event that starts the use case | +| Preconditions | Facts that must already be true | +| Main success flow | Normal sequence that reaches the goal | +| Alternative flows | Valid variations that may still succeed | +| Failure flows | Conditions that prevent success | +| Postconditions | Facts guaranteed after success or failure | +| Authorization | Server-side access decision | +| Idempotency/concurrency | Behavior under retries or competing requests | +| Business rules | Stable rule identifiers exercised by the use case | + +## Actor model + +Use cases refer to roles rather than assuming a separate stored entity for each label. + +| Role | Meaning in a use case | +| --- | --- | +| User | Authenticated identity | +| Host | User responsible for a scheduling resource or meeting | +| Guest | Person requesting a direct booking | +| Participant | Person responding to or attending a group scheduling process | +| Workspace member | Proposed user authorized through membership | +| System | SlotSyncro application and durable processing | +| Provider | External email, calendar, or conferencing system | + +## Use-case catalogue + +| ID | Use case | Status | Primary actor | +| --- | --- | --- | --- | +| UC-ID-001 | Complete first-user onboarding | Proposed | User | +| UC-SCH-001 | Configure recurring availability | Current | Host | +| UC-SCH-002 | Create an event type | Current | Host | +| UC-BKG-001 | Create a direct booking | Current with limitations | Guest | +| UC-POL-001 | Create and publish a poll | Partial | Host | +| UC-POL-002 | Submit or revise a poll response | Partial | Participant | +| UC-POL-003 | Finalize a poll into a meeting | Proposed | Host | +| UC-MTG-001 | Cancel a meeting | Proposed | Authorized actor | +| UC-MTG-002 | Reschedule a meeting directly | Proposed | Authorized actor | +| UC-MTG-003 | Reschedule a meeting by consensus | Proposed | Host | + +--- + +## UC-ID-001: Complete first-user onboarding + +**Status:** Proposed +**Primary actor:** Newly authenticated user +**Goal:** Establish the minimum identity and scheduling configuration required to create a usable resource + +### Trigger + +The user completes OAuth authentication, and the system determines that onboarding is incomplete. + +### Preconditions + +- The external identity has been authenticated successfully. +- A local user account exists or can be created safely. +- The user has not completed the current onboarding version. + +### Main success flow + +1. System presents the identity information received from the provider. +2. User confirms or updates their display name. +3. User chooses a public username. +4. System normalizes and validates the username. +5. System verifies that the username is available and not reserved. +6. User confirms an IANA timezone. +7. User configures or accepts initial weekly availability. +8. System saves the onboarding configuration. +9. System marks the current onboarding version complete. +10. System directs the user to create a direct-booking resource or group poll. + +### Alternative flows + +#### A1: Provider did not supply a name + +User must enter a display name before continuing. + +#### A2: Suggested username is unavailable + +System proposes alternatives without discarding the other onboarding fields. + +#### A3: User skips availability configuration + +If skipping is permitted, the system clearly marks direct-booking setup as incomplete. Skipping does not create misleading public availability. + +### Failure flows + +- Username becomes unavailable between validation and save. +- Timezone is not a valid supported IANA timezone. +- User account is disabled or deleted during onboarding. +- Persistence fails before onboarding is complete. + +### Postconditions + +#### On success + +- User has a valid public identity and timezone. +- Username uniqueness is enforced durably. +- Initial availability is either configured or explicitly incomplete. +- Onboarding completion is recorded. + +#### On failure + +- Onboarding remains incomplete. +- The user can retry without creating duplicate personal resources. + +### Authorization + +The authenticated user may update only their own onboarding profile. Client-submitted user IDs are not trusted as authorization. + +### Idempotency and concurrency + +- Repeating completion must not create duplicate personal workspaces or default resources. +- Username availability must be protected by a unique database constraint, not only a prior query. + +### Preliminary business rules + +- **BR-ID-001:** A public username is unique after normalization. +- **BR-ID-002:** A user has one current onboarding state. +- **BR-TIME-001:** User timezone must be a valid IANA timezone identifier. + +--- + +## UC-SCH-001: Configure recurring availability + +**Status:** Current +**Primary actor:** Host +**Goal:** Define recurring working windows used to generate scheduling candidates + +### Trigger + +Host opens availability settings and submits a weekly schedule. + +### Preconditions + +- Host is authenticated. +- Host account exists. +- Submitted timezone and schedule have passed structural validation. + +### Main success flow + +1. Host selects their scheduling timezone. +2. Host enables or disables weekdays. +3. Host adds one or more non-empty time windows to enabled days. +4. Host submits the schedule. +5. System authenticates the request again. +6. System validates day values and `HH:mm` time ranges. +7. System verifies that each start time precedes its end time. +8. System persists the timezone and day windows. +9. System revalidates affected scheduling pages. +10. UI confirms that the schedule was saved. + +### Alternative flows + +#### A1: Multiple windows on one day + +Host may configure separated windows, such as `09:00–12:00` and `14:00–17:00`. + +#### A2: Day is unavailable + +No scheduling candidates are generated for the disabled day. + +### Failure flows + +- User is unauthenticated. +- Submitted user identity does not match the authenticated user. +- Timezone is invalid. +- Day or time format is invalid. +- Start is not earlier than end. +- Persistence fails. + +### Postconditions + +#### On success + +- The host has one effective recurring configuration for each represented weekday. +- Candidate generation uses the saved timezone context. + +#### On failure + +- Previously valid availability remains authoritative. + +### Authorization + +Only the authenticated owner may change their personal availability. Future workspace availability requires a separately defined permission model. + +### Idempotency and concurrency + +- Submitting the same schedule repeatedly should produce the same effective configuration. +- A single save should not leave a partially updated week. + +### Preliminary business rules + +- **BR-SCH-001:** A recurring window has `startTime < endTime`. +- **BR-SCH-002:** Disabled days produce no candidates. +- **BR-SCH-003:** A host has at most one effective day configuration per weekday. +- **BR-TIME-002:** Recurring local hours retain the timezone in which they were defined. + +--- + +## UC-SCH-002: Create an event type + +**Status:** Current +**Primary actor:** Host +**Goal:** Create a reusable direct-booking configuration with a public link + +### Trigger + +Host submits the create-event-type form. + +### Preconditions + +- Host is authenticated. +- Host account exists. +- Host has permission to create resources in the owning scope. + +### Main success flow + +1. Host enters title, slug, duration, optional description, and buffers. +2. System validates field formats and ranges. +3. System normalizes the slug according to the public URL policy. +4. System verifies that the slug is not already used in the owning scope. +5. System creates the active event type. +6. System revalidates the event-type management page. +7. UI displays the new event type and public link. + +### Alternative flows + +#### A1: Host accepts generated slug + +UI derives a slug from the title, but the server still validates it independently. + +#### A2: Event type is created before availability + +Creation may succeed, but UI must communicate that the booking link is not ready to offer slots. + +### Failure flows + +- User is unauthenticated. +- Validation fails. +- Slug already exists. +- Concurrent creation claims the same slug after the availability check. +- Persistence fails. + +### Postconditions + +#### On success + +- Event type belongs to exactly one current owning scope. +- Public slug is unique inside that scope. +- Public booking configuration can be retrieved using owner identity and slug. + +#### On failure + +- No partial event type exists. + +### Authorization + +The authenticated host is derived from the session. Ownership is not accepted solely from form input. + +### Idempotency and concurrency + +- Slug uniqueness must be protected by a database constraint. +- Repeated submission is not inherently idempotent unless supplied an idempotency key; duplicate slug protection prevents the most visible duplication. + +### Preliminary business rules + +- **BR-EVT-001:** Event-type slug is unique within its owning scope. +- **BR-EVT-002:** Duration and buffers remain within supported limits. +- **BR-EVT-003:** Archived event types cannot accept new bookings. + +--- + +## UC-BKG-001: Create a direct booking + +**Status:** Current with limitations +**Primary actor:** Guest +**Supporting actors:** Host, email provider +**Goal:** Reserve an available event-type slot exactly once and receive an authoritative result + +### Trigger + +Guest confirms a selected slot and submits their details. + +### Preconditions + +- Public event type exists and is active. +- Host exists. +- Submitted event type belongs to the submitted host. +- Requested start is a valid absolute timestamp. +- Guest name, email, and timezone are structurally valid. + +### Main success flow + +1. System validates the submitted booking input. +2. System loads the event type and host from trusted persistence. +3. System verifies ownership and active status. +4. System calculates end time using the persisted event duration. +5. System checks for overlapping active bookings. +6. System creates a unique calendar UID. +7. System creates the accepted booking. +8. System generates an ICS event. +9. System attempts confirmation-email delivery. +10. System revalidates affected booking and public availability pages. +11. System returns booking success and a separate email-delivery outcome. +12. UI displays authoritative booking confirmation. + +### Alternative flows + +#### A1: Email delivery fails + +- Booking remains successful. +- System records or logs the delivery failure. +- UI explains that the meeting is confirmed but email delivery failed. + +#### A2: Host has no email address + +ICS generation omits the organizer email rather than inventing one. + +#### A3: Guest is authenticated + +UI may prefill guest identity, but server validation remains unchanged. + +### Failure flows + +- Input validation fails. +- Event type or host is missing or inactive. +- Event type does not belong to host. +- Slot overlaps a pending or accepted booking. +- Booking persistence fails. +- A concurrent request books the same slot after the conflict query but before insertion. + +The final concurrency case is a documented current limitation requiring a stronger persistence strategy. + +### Postconditions + +#### On success + +- Exactly one accepted booking should represent the reservation. +- Booking start and end are stored as absolute timestamps. +- Calendar UID is unique. +- Email outcome does not change booking truth. +- Slot is excluded from subsequent availability results. + +#### On failure before booking creation + +- No booking or notification should be created. + +#### On notification failure after booking creation + +- Booking remains accepted. +- Failure is recoverable independently. + +### Authorization + +Public booking does not require authentication. The system therefore authorizes the operation through resource validity and applies server-side validation, rate limiting, and abuse controls rather than account membership. + +### Idempotency and concurrency + +- Proposed: accept a client-generated idempotency key for booking submission. +- Proposed: enforce a durable strategy preventing overlapping active bookings under concurrency. +- Notification retries must reuse the booking and calendar identity. + +### Preliminary business rules + +- **BR-BKG-001:** Booking event type must belong to the selected host. +- **BR-BKG-002:** Archived event types reject new bookings. +- **BR-BKG-003:** Active bookings for one host must not overlap according to the scheduling policy. +- **BR-BKG-004:** Booking end derives from trusted event-type duration. +- **BR-BKG-005:** A successful booking is independent of notification delivery. +- **BR-NOT-001:** External notification failure does not roll back committed business state. +- **BR-TIME-003:** Scheduled instances are stored as absolute timestamps. + +--- + +## UC-POL-001: Create and publish a poll + +**Status:** Partial +**Primary actor:** Host +**Goal:** Publish a set of candidate times for participant preference collection + +### Trigger + +Host submits poll details and candidate times. + +### Preconditions + +- Host is authenticated. +- Host has permission to create a poll in the owning scope. +- Poll title and candidate input pass structural validation. + +### Current success flow + +1. Host enters title and optional description. +2. Host selects one date and one or more fixed start hours. +3. System validates the input. +4. System creates a unique slug using normalized title and a random suffix. +5. System converts submitted local-looking values into timestamps. +6. System creates the poll and candidate records together. +7. System redirects host to the public poll URL. + +### Target success flow additions + +1. Host specifies duration and organizer timezone explicitly. +2. Host may add candidates across several dates. +3. System validates candidate uniqueness, duration, and future constraints. +4. Host configures access, result visibility, participant requirements, and deadline. +5. Poll begins in a defined lifecycle state. +6. Host receives explicit share and invitation actions. + +### Alternative flows + +#### A1: Save as draft + +Poll is stored but cannot receive public responses until published. + +#### A2: Generate candidates + +System proposes times using scheduling rules; host reviews them before publication. + +#### A3: Open-link poll + +Participants may respond without prior invitation, subject to abuse controls. + +### Failure flows + +- User is unauthenticated or unauthorized. +- No candidates are supplied. +- Candidate occurs in the past. +- Candidate timezone cannot be interpreted. +- Candidate times duplicate or overlap contrary to policy. +- Slug collision occurs. +- Poll creation succeeds but candidate creation fails. + +The final condition must be prevented with one transaction or nested atomic write. + +### Postconditions + +#### On success + +- Poll and all candidates share one owner and lifecycle. +- Every candidate belongs to the poll. +- Published poll has a resolvable public identifier. +- Organizer timezone and duration are unambiguous in the target model. + +#### On failure + +- No incomplete published poll remains. + +### Authorization + +Authenticated identity and owning scope are derived server-side. Future workspace creation requires membership permission. + +### Idempotency and concurrency + +- Proposed: creation request carries an idempotency key. +- Slug has a durable uniqueness constraint. +- Poll and candidates are created atomically. + +### Preliminary business rules + +- **BR-POL-001:** A published poll contains at least one candidate. +- **BR-POL-002:** Every candidate belongs to exactly one poll. +- **BR-POL-003:** Poll slug is globally unique under the current URL design. +- **BR-POL-004:** Only draft or open polls may change candidates according to policy. +- **BR-TIME-004:** Poll candidate construction requires explicit organizer timezone context. + +--- + +## UC-POL-002: Submit or revise a poll response + +**Status:** Partial +**Primary actor:** Participant +**Goal:** Save one coherent set of preferences for the poll + +### Trigger + +Participant submits preference choices. + +### Preconditions + +- Poll exists. +- Target behavior: poll is open and before its deadline. +- Participant has access through open-link policy or a valid invitation. +- Submitted candidate IDs belong to the poll. +- Submitted preference values are supported. + +### Current success flow + +1. Participant enters name and optional email. +2. Participant assigns `YES`, `IF_NEEDED`, or `NO` to candidates. +3. System normalizes participant name. +4. System deletes prior votes matching the same case-insensitive name. +5. System creates the submitted votes in one transaction. +6. UI refreshes and displays updated aggregate results. + +### Target success flow + +1. System resolves participant identity from invitation or open-link input. +2. System verifies poll state, deadline, access, and candidate membership. +3. Participant explicitly answers required candidates. +4. System upserts one response set for that participant. +5. System records response completion time. +6. UI confirms saved response and provides an edit path while permitted. + +### Alternative flows + +#### A1: Revise response + +Participant replaces their previous preference set while the poll is open. + +#### A2: Partial response is allowed + +Unanswered candidates are stored or interpreted distinctly from `NO`. + +#### A3: Open-link identity + +System may issue a private edit token after first submission rather than relying on name alone. + +### Failure flows + +- Poll is missing, finalized, cancelled, or expired. +- Invitation token is missing, invalid, revoked, or expired. +- Submitted candidate belongs to another poll. +- Duplicate candidate appears in one response. +- Preference value is unsupported. +- Required answers are missing. +- Persistence transaction fails. + +### Postconditions + +#### On success + +- One effective preference exists per participant and candidate. +- All saved candidates belong to the same poll as the participant response. +- Revising replaces effective preferences without duplicating the participant. +- Aggregate results reflect the new effective response. + +#### On failure + +- Previous complete response remains authoritative. +- No partial replacement is visible. + +### Authorization + +Private polls authorize through a secret invitation or edit token. Open polls authorize participation through poll policy but still require validation, rate limiting, and abuse protection. + +### Idempotency and concurrency + +- Repeating the same response should produce the same effective preference set. +- Concurrent revisions from one participant require a defined last-write or version-conflict policy. + +### Preliminary business rules + +- **BR-POL-005:** Only open polls accept responses. +- **BR-POL-006:** One participant has at most one effective vote per candidate. +- **BR-POL-007:** A vote's participant, candidate, and poll must share the same poll boundary. +- **BR-POL-008:** Unanswered is distinct from `NO`. +- **BR-POL-009:** Finalized, expired, and cancelled polls reject revisions. + +--- + +## UC-POL-003: Finalize a poll into a meeting + +**Status:** Proposed +**Primary actor:** Host +**Supporting actors:** Participants, notification/calendar processing +**Goal:** Commit one poll candidate as the authoritative meeting time + +### Trigger + +Host confirms finalization of a selected candidate, or an approved automatic policy triggers finalization. + +### Preconditions + +- Poll exists and is open. +- Actor is authorized to finalize the poll. +- Candidate exists and belongs to the poll. +- Poll has not already produced an active meeting. +- Candidate satisfies configured required-participant and conflict policy, or an authorized override is recorded. + +### Main success flow + +1. System authenticates and authorizes the actor. +2. System loads current poll state, candidate, participants, and effective responses. +3. System recalculates conflicts and recommendation facts using authoritative data. +4. System presents or validates the final decision and any overrides. +5. Within one transaction, system: + 1. verifies poll is still open, + 2. records selected candidate, + 3. creates one meeting and its participants, + 4. marks poll finalized, + 5. records durable notification and calendar work. +6. Transaction commits. +7. System returns the meeting result without waiting for every external provider. +8. Background processing delivers invitations and synchronizes calendars. + +### Alternative flows + +#### A1: Finalize before everyone responds + +Allowed only when the configured decision policy or explicit host override permits it. + +#### A2: Select a non-recommended candidate + +Host may choose another valid candidate. System stores recommendation facts and optional override reason for auditability. + +#### A3: Automatic finalization + +Background system acts under a previously configured policy and records that automation initiated the decision. + +### Failure flows + +- Actor is unauthorized. +- Poll is no longer open. +- Candidate does not belong to poll. +- Candidate became unavailable. +- Required-participant rule fails. +- Another request finalized the poll concurrently. +- Meeting creation or durable work recording fails before commit. +- Email/calendar provider fails after commit. + +Provider failure after commit is not a finalization failure. + +### Postconditions + +#### On success + +- Poll is finalized with exactly one selected candidate. +- Poll has at most one active resulting meeting. +- Voting and candidate mutation are locked. +- Meeting participants derive from the poll's participant policy. +- Notification/calendar work is durable and independently retryable. + +#### On failure before commit + +- Poll remains open. +- No meeting or partial durable work remains. + +#### On provider failure after commit + +- Poll and meeting remain finalized. +- Provider work records a retryable failure. + +### Authorization + +Current personal scope: poll owner only. Future workspace scope may allow administrators or explicitly delegated members. Automatic finalization requires an enabled policy established by an authorized actor. + +### Idempotency and concurrency + +- A poll can produce at most one active meeting. +- Repeated finalization returns the existing result or a stable already-finalized outcome. +- Poll state transition and meeting creation require transactional protection. + +### Preliminary business rules + +- **BR-POL-010:** Only an open poll may transition to finalized. +- **BR-POL-011:** A finalized poll references exactly one candidate belonging to that poll. +- **BR-POL-012:** A poll produces at most one active resulting meeting. +- **BR-MTG-001:** A meeting records its scheduling source. +- **BR-NOT-002:** Required external work is recorded durably before asynchronous delivery. + +--- + +## UC-MTG-001: Cancel a meeting + +**Status:** Proposed +**Primary actor:** Authorized host, guest, or participant +**Goal:** End a scheduled commitment and notify affected parties consistently + +### Trigger + +Authorized actor confirms cancellation. + +### Preconditions + +- Meeting exists. +- Meeting is in a cancellable state. +- Actor has cancellation permission through identity, membership, or valid management token. + +### Main success flow + +1. System resolves the actor and authorization method. +2. System loads current meeting state. +3. Actor reviews cancellation impact and optionally provides a reason. +4. System changes meeting state to cancelled. +5. System records cancellation time, actor, and reason. +6. System records durable notification and calendar-cancellation work. +7. System releases internal holds or future availability conflicts according to policy. +8. UI confirms cancellation. +9. Background processing updates participants and provider calendars. + +### Alternative flows + +#### A1: Already cancelled + +System returns the existing cancelled state without creating duplicate work. + +#### A2: Guest cancellation + +Guest uses a revocable secret management token and may be subject to notice-window policy. + +### Failure flows + +- Actor is unauthorized. +- Management token is invalid or revoked. +- Meeting is already completed or otherwise non-cancellable. +- State changes concurrently. +- Persistence fails before durable work is recorded. + +### Postconditions + +#### On success + +- Meeting is cancelled exactly once. +- Cancellation audit information is retained. +- Meeting no longer blocks availability according to the cancellation policy. +- Participant update work is durable. + +#### On provider failure + +- Internal cancellation remains authoritative. +- Provider synchronization is retryable and visibly failed. + +### Authorization + +Cancellation policy must distinguish host, workspace administrator, authenticated participant, and secret-token guest. Possession of a meeting ID alone is never authorization. + +### Idempotency and concurrency + +- Repeated cancellation is safe. +- Only one valid state transition wins under concurrent cancellation/reschedule attempts. + +### Preliminary business rules + +- **BR-MTG-002:** Only meetings in cancellable states may transition to cancelled. +- **BR-MTG-003:** Cancellation records actor and timestamp. +- **BR-MTG-004:** Cancelled meetings do not block future availability according to policy. + +--- + +## UC-MTG-002: Reschedule a meeting directly + +**Status:** Proposed +**Primary actor:** Authorized host, guest, or participant +**Goal:** Replace the meeting time while preserving one authoritative meeting identity + +### Trigger + +Authorized actor selects and confirms a replacement time. + +### Preconditions + +- Meeting exists and is reschedulable. +- Actor has permission to reschedule. +- Replacement time is valid and currently available. +- Meeting version has not changed since the actor began the operation. + +### Main success flow + +1. System authorizes actor. +2. System calculates valid replacement candidates. +3. Actor selects a replacement. +4. System rechecks conflicts and current meeting version. +5. System updates the meeting's scheduled interval and records history. +6. System records calendar-update and notification work. +7. System returns updated meeting state. +8. Background processing updates the existing provider event identity. + +### Alternative flows + +#### A1: Host proposes time requiring guest confirmation + +Meeting remains at its current time until the guest accepts, or enters an explicit proposed state that does not create two authoritative times. + +#### A2: Guest requests reschedule + +Policy may require host approval rather than immediate replacement. + +### Failure flows + +- Actor is unauthorized. +- Meeting is cancelled, completed, or locked. +- Replacement conflicts with another active commitment. +- Meeting changed since candidate selection. +- Provider update fails after internal commit. + +### Postconditions + +#### On success + +- One meeting remains authoritative. +- Previous interval is retained in history. +- Calendar identity is reused where provider protocol permits. +- Participants receive update work. + +### Authorization + +Permission and approval requirements may differ for host and guest. They must be explicit policy rather than inferred from UI location. + +### Idempotency and concurrency + +- Meeting version or equivalent optimistic concurrency check prevents lost updates. +- Repeating the same accepted reschedule produces one effective interval change. + +### Preliminary business rules + +- **BR-MTG-005:** A reschedule retains meeting identity and records previous schedule history. +- **BR-MTG-006:** Replacement time must pass the current conflict policy. +- **BR-MTG-007:** Concurrent updates must not silently overwrite one another. + +--- + +## UC-MTG-003: Reschedule a meeting by consensus + +**Status:** Proposed +**Primary actor:** Host +**Supporting actors:** Existing meeting participants +**Goal:** Use a poll to select a replacement time without creating a duplicate active meeting + +### Trigger + +Host chooses to find a new time with the existing participants. + +### Preconditions + +- Meeting exists and is reschedulable. +- Host has permission to initiate consensus rescheduling. +- No other active rescheduling process exists for the meeting unless policy permits it. + +### Main success flow + +1. System copies meeting context, duration, participants, and relevant constraints into a rescheduling poll. +2. Host reviews or generates candidate times. +3. System publishes the poll and invites existing participants. +4. Participants submit preferences. +5. Host or policy finalizes a replacement candidate. +6. System updates the original meeting interval and history. +7. System finalizes the rescheduling poll and links it to the meeting revision. +8. System records notification and calendar-update work. + +### Alternative flows + +#### A1: No candidate reaches decision criteria + +Host adds candidates, extends the deadline, or cancels the rescheduling process. Original meeting remains authoritative unless explicitly cancelled. + +#### A2: Original meeting occurs before finalization + +System expires the rescheduling process according to policy. + +### Failure flows + +- Another rescheduling process is active. +- Original meeting is cancelled or changed while poll is open. +- Selected candidate becomes unavailable. +- Poll finalization succeeds but meeting update fails; transaction must prevent this split state. + +### Postconditions + +#### On success + +- Original meeting identity remains authoritative with a new interval. +- Rescheduling poll records its relationship to the meeting revision. +- No duplicate active meeting is created. + +#### On abandoned or failed rescheduling + +- Original meeting remains unchanged unless separately cancelled. + +### Authorization + +Only an actor authorized to reschedule the meeting may initiate or finalize its consensus process. + +### Idempotency and concurrency + +- One active consensus-rescheduling process per meeting is the proposed default. +- Finalization and meeting update are atomic. + +### Preliminary business rules + +- **BR-MTG-008:** A meeting has at most one active consensus-rescheduling process by default. +- **BR-MTG-009:** Rescheduling poll finalization updates the existing meeting rather than creating a second active commitment. +- **BR-MTG-010:** Abandoning rescheduling does not change the original meeting. + +## Cross-cutting rules discovered + +These preliminary rules appear across several use cases and will be normalized in the next business-rules document. + +### Authorization + +- **BR-AUTH-001:** Every mutation revalidates identity or public authorization server-side. +- **BR-AUTH-002:** Resource identifiers prove which record is requested, not who may mutate it. +- **BR-AUTH-003:** Workspace access requires current membership and sufficient role. + +### Time + +- **BR-TIME-001:** User timezone is a valid IANA identifier. +- **BR-TIME-002:** Recurring local schedules retain their defining timezone. +- **BR-TIME-003:** Scheduled instances are stored as absolute timestamps. +- **BR-TIME-004:** Poll candidate construction has explicit timezone context. + +### Notifications and integrations + +- **BR-NOT-001:** External delivery failure does not roll back committed business state. +- **BR-NOT-002:** Required asynchronous work is recorded durably before delivery. +- **BR-NOT-003:** Retried provider operations reuse stable internal and provider identities. + +### Reliability + +- **BR-REL-001:** Public creation operations require abuse controls appropriate to their risk. +- **BR-REL-002:** Retried commands must not create duplicate business outcomes. +- **BR-REL-003:** Multi-record lifecycle transitions are atomic where partial state would be invalid. + +## Open decisions exposed by the use cases + +1. Does direct booking create a `Booking` that is itself the scheduled commitment, or does it create a separate `Meeting`? +2. Does onboarding create a personal workspace immediately? +3. Are open-link and invitation-only polls both supported? +4. How is participant identity represented for accountless users? +5. What is the exact lifecycle state machine for polls and meetings? +6. Which database strategy prevents overlapping active bookings under concurrency? +7. Which actors may cancel or reschedule, and how are accountless actors authorized? +8. Is meeting history modeled as revisions, lifecycle events, or audit records? +9. What durable-job mechanism will support notification and calendar work? +10. When can an automatic finalization policy act on behalf of a host? + +These decisions must be addressed before the proposed ER model is treated as accepted. + +## Next documentation step + +Create `business-rules.md` to: + +1. Remove duplicate preliminary rules. +2. Give each rule one authoritative definition. +3. Classify rules as current, target, or unresolved. +4. Connect rules to use cases and future tests. +5. Identify which rules require database constraints versus application enforcement. + +The current ER diagram can then be documented accurately, followed by a proposed model tested against these use cases and rules. + diff --git a/docs/user-journeys.md b/docs/user-journeys.md new file mode 100644 index 0000000..9527cd2 --- /dev/null +++ b/docs/user-journeys.md @@ -0,0 +1,565 @@ +# User Journeys + +**Document status:** Initial draft +**Scope:** Current journeys, known breaks, and proposed target journeys + +## Purpose + +A user journey describes the outcome a person is trying to reach across several interactions. It is broader than a screen and less technically precise than a use case. + +Journeys help answer: + +- Why does the user enter the product? +- Which steps must feel connected? +- Where does the current experience stop prematurely? +- Which handoffs require durable data or notifications? +- What must the later use cases and domain model support? + +This document does not define database tables. It provides the evidence from which business rules and entities will be derived. + +## Journey status + +| Label | Meaning | +| --- | --- | +| **Current** | The journey can be completed in the present application. | +| **Partial** | Some steps work, but the intended outcome or recovery path is incomplete. | +| **Proposed** | Target experience to be designed and implemented later. | + +## Actors and roles + +An actor is a role participating in a journey. It is not automatically a database entity. + +| Actor | Description | Account required? | +| --- | --- | --- | +| Visitor | Unauthenticated person exploring SlotSyncro | No | +| User | Authenticated account holder | Yes | +| Host | Person responsible for an event type, poll, or meeting | Usually | +| Guest | Person reserving time through a direct-booking link | No | +| Poll participant | Person expressing preferences in a poll | Not currently | +| Required participant | Proposed participant whose availability can block finalization | Not necessarily | +| Optional participant | Proposed participant whose availability informs but does not block a decision | Not necessarily | +| Workspace member | Proposed user collaborating within a personal or team workspace | Yes | +| Workspace administrator | Proposed member managing access and shared resources | Yes | +| Background system | Proposed scheduled processing for deadlines, reminders, and retries | Not applicable | +| Email provider | External system delivering transactional email | Not applicable | +| Calendar provider | Proposed external source of free/busy data and calendar events | Not applicable | + +The same person can hold several roles. A user may be the host of one poll and a participant in another. Role and identity must therefore remain separate concepts. + +## Journey map + +```mermaid +flowchart LR + Start[Person needs to coordinate time] + Start --> Direct{Who chooses?} + Direct -->|One guest chooses from host availability| Booking[Direct booking] + Direct -->|A group must reach agreement| Poll[Consensus poll] + Booking --> Meeting[Confirmed meeting] + Poll --> Decision[Recommendation and finalization] + Decision --> Meeting + Meeting --> Manage[Manage, cancel, or reschedule] +``` + +The central product goal is for both scheduling paths to converge on a reliable meeting outcome. + +--- + +## J-01: First authentication and onboarding + +**Current status:** Partial +**Primary actor:** Visitor becoming a user +**Goal:** Reach a useful scheduling setup after the first authentication + +### Current journey + +```text +Visitor opens SlotSyncro + -> chooses OAuth sign-in + -> provider authenticates the visitor + -> Auth.js creates or retrieves the user + -> user returns to the application + -> user sees the signed-in home experience +``` + +### Current friction + +- First sign-in and returning sign-in are not presented as distinct experiences. +- There is no guided username selection. +- Timezone and initial availability are not confirmed during onboarding. +- The user is not guided toward creating an event type or poll. +- The header references a dashboard route that does not currently exist. +- A new user can reach several empty states without understanding the recommended order. + +### Proposed target journey + +```text +Visitor selects Get started + -> authenticates with an OAuth provider + -> system detects incomplete onboarding + -> user confirms display name + -> chooses a public username + -> confirms timezone + -> configures initial weekly availability + -> chooses Direct booking or Group scheduling + -> creates the first scheduling resource + -> reaches the relevant dashboard with a shareable result +``` + +### Meaningful completion + +The user should leave onboarding with at least one usable scheduling resource, not merely an account record. + +### Later design questions + +- Is onboarding considered complete before or after the first resource is created? +- Is a personal workspace created at account creation or onboarding completion? +- What happens when an OAuth provider does not supply a usable name or email? +- How are username conflicts and reserved names handled? + +--- + +## J-02: Configure direct scheduling + +**Current status:** Current with partial management experience +**Primary actor:** Host +**Goal:** Produce a public link through which guests can reserve valid time + +### Current journey + +```text +Host signs in + -> configures weekly availability and timezone + -> creates an event type + -> receives a public slug + -> copies or opens the booking link + -> shares the link outside SlotSyncro +``` + +### What works + +- Weekly availability supports multiple windows per day. +- Event types contain title, duration, description, slug, and buffers. +- Event types can be activated, deactivated, and deleted. +- The host can copy or open a public booking link. + +### Current friction + +- Availability and event-type setup are separate pages without a guided sequence. +- The UI does not clearly indicate whether a new event type has usable availability. +- Public slot generation currently does not apply the event type's configured buffers. +- Link construction and navigation do not consistently preserve locale. +- There is no preview mode that avoids creating a real booking. + +### Proposed improvement + +Present a scheduling-readiness checklist: + +```text +✓ Public identity configured +✓ Timezone confirmed +✓ Weekly availability configured +✓ Event type active +✓ Public link ready to share +``` + +### Meaningful completion + +The host has an active, valid, previewed booking link that can produce conflict-free reservations. + +--- + +## J-03: Guest completes a direct booking + +**Current status:** Current; lifecycle management remains partial +**Primary actor:** Guest +**Supporting actors:** Host, email provider +**Goal:** Reserve a valid meeting time and receive reliable confirmation + +### Current journey + +```mermaid +sequenceDiagram + actor Guest + participant UI as Public booking UI + participant App as SlotSyncro + participant DB as PostgreSQL + participant Email as Email provider + + Guest->>UI: Open host event link + UI->>Guest: Show dates and guest-local slots + Guest->>UI: Select slot and enter details + UI->>App: Submit booking + App->>DB: Validate host/event and check conflict + App->>DB: Create booking + App->>Email: Send confirmation with ICS + App-->>UI: Return booking and email outcome + UI-->>Guest: Show confirmed meeting +``` + +### Main experience + +1. Guest opens an active event-type link. +2. Guest sees host metadata, duration, description, and detected timezone. +3. Guest selects an available date and time. +4. Guest provides name, email, and optional notes. +5. The system validates the request again on the server. +6. The system checks for an overlapping accepted or pending booking. +7. The system creates the booking. +8. The system generates an ICS invitation and attempts email delivery. +9. The UI confirms the meeting and separately communicates email delivery status. + +### Existing recovery behavior + +- Invalid form data returns field-level feedback. +- An inactive event type or host stops booking. +- A newly conflicting slot asks the guest to choose another time. +- Email delivery failure does not invalidate the successfully created booking. + +### Remaining friction + +- Conflict check and creation are not protected by a final database-level concurrency strategy. +- There is no guest-facing booking detail URL. +- Guest cannot cancel or reschedule. +- Host does not have a complete management workflow. +- The booking dashboard does not deliberately format time in the host timezone. +- External calendar conflicts are not considered. + +### Meaningful completion + +The booking exists exactly once, the selected time is no longer available, and the guest sees authoritative confirmation even if a secondary notification fails. + +--- + +## J-04: Host reviews and manages a booking + +**Current status:** Partial +**Primary actor:** Host +**Goal:** Understand and manage upcoming scheduled commitments + +### Current journey + +```text +Host signs in + -> opens Bookings + -> sees upcoming booking cards + -> reviews event, guest, status, time, email, and notes + -> journey ends +``` + +### Current friction + +- No booking detail page +- No cancellation +- No rescheduling +- No manual resend of a failed confirmation +- No past-meeting view +- No search or filters +- No explicit host-timezone conversion +- No audit or notification history + +### Proposed target journey + +```text +Host opens Meetings + -> filters upcoming, past, or cancelled meetings + -> opens meeting detail + -> reviews participants, source, notes, and delivery state + -> performs an authorized action + -> cancel + -> propose reschedule + -> resend notification + -> update location + -> system records the lifecycle change + -> affected participants receive an update +``` + +### Meaningful completion + +The host can manage the complete meeting lifecycle without creating contradictory calendar, notification, or internal states. + +--- + +## J-05: Host creates and shares a group poll + +**Current status:** Partial +**Primary actor:** Host +**Goal:** Collect group availability for several candidate times + +### Current journey + +```text +Host signs in + -> opens Create Poll from the home page + -> enters title and optional description + -> selects one date + -> selects fixed one-hour candidates + -> submits + -> system creates a poll and slug + -> host is redirected to the public poll + -> host copies the URL manually +``` + +### What works + +- Poll and candidate records are created together. +- Poll slug receives a random suffix. +- Public poll shows host, description, candidates, heatmap, and voting form. +- Host authentication is required for poll creation. + +### Current friction + +- Polls are absent from dashboard navigation and have no management list. +- Only one date and fixed hourly candidates are supported. +- Poll duration is fixed implicitly to one hour. +- Organizer timezone is not stored explicitly for candidate construction. +- No participant invitation list or response deadline exists. +- No explicit copy/share experience follows creation. +- Poll has no status such as draft, open, finalized, or expired. + +### Proposed target journey + +```text +Host chooses Group scheduling + -> enters meeting details, duration, and timezone + -> adds several dates or asks SlotSyncro to suggest candidates + -> optionally identifies required and optional participants + -> sets response deadline and result visibility + -> previews candidate times across participant timezones + -> publishes poll + -> receives share link and invitation options + -> tracks participant response progress +``` + +### Meaningful completion + +The host has a published, timezone-safe poll with identifiable candidate times and a clear path to monitor responses. + +--- + +## J-06: Participant votes in a poll + +**Current status:** Partial +**Primary actor:** Poll participant +**Goal:** Communicate usable preferences without creating an account + +### Current journey + +```text +Participant opens poll URL + -> sees host, poll details, aggregate heatmap, and candidates + -> enters name and optional email + -> assigns Yes, If needed, or No to candidates + -> submits votes + -> page refreshes + -> updated heatmap appears +``` + +### What works + +- An account is not required. +- Three preference levels are supported. +- Votes are written in one database transaction. +- Submitting again with the same case-insensitive name replaces earlier responses. + +### Current friction and risk + +- Every candidate begins as `YES`, which can create accidental availability. +- Success is represented by a refresh rather than a clear confirmation. +- Free-form name acts as identity and can collide or be impersonated. +- The action does not yet model a poll participant with an invitation token. +- Candidate-to-poll membership requires stronger validation in the submission boundary. +- Aggregate results are visible before voting with no host-configurable privacy mode. +- Missing responses are not clearly distinguished from negative responses. + +### Proposed target journey + +```text +Participant opens public or private invitation link + -> sees meeting context, deadline, timezone, and local candidate times + -> identifies themselves or is recognized by invitation token + -> explicitly answers each candidate + -> optionally adds a comment or suggests another time + -> reviews response summary + -> submits + -> receives confirmation and edit link + -> later receives the finalized result +``` + +### Meaningful completion + +The participant knows their response was saved, can correct it while voting remains open, and understands what happens next. + +--- + +## J-07: Host finalizes a poll into a meeting + +**Current status:** Proposed +**Primary actor:** Host +**Supporting actors:** Participants, background system, email provider, calendar provider +**Goal:** Turn group preferences into one authoritative scheduled commitment + +### Target journey + +```mermaid +sequenceDiagram + actor Host + participant Poll as Polling module + participant Meeting as Meetings module + participant DB as PostgreSQL + participant Jobs as Notification/calendar work + + Host->>Poll: Review responses and recommendation + Poll-->>Host: Explain consensus, conflicts, and fairness + Host->>Poll: Finalize selected candidate + Poll->>DB: Validate ownership, state, and candidate + Poll->>Meeting: Create meeting from poll + Meeting->>DB: Store meeting and participants + Poll->>DB: Mark poll FINALIZED + Poll->>Jobs: Record durable delivery work + Poll-->>Host: Show confirmed meeting +``` + +### Target experience + +1. Host sees response progress and ranked candidates. +2. The system explains the recommended candidate. +3. Host selects a candidate and reviews participant impact. +4. The system revalidates authorization, poll state, and candidate availability. +5. Poll finalization and meeting creation occur atomically. +6. Further voting is locked. +7. Durable notification and calendar work is recorded. +8. Host sees meeting details without waiting for every external provider. +9. Participants receive the final outcome. + +### Alternative paths + +- Candidate became unavailable: keep poll open and ask host to choose again. +- Required participant cannot attend: warn or block according to policy. +- Some participants have not responded: apply host-configured decision policy. +- Email or calendar provider fails: meeting remains finalized; retryable work records failure. +- Host repeats the request: idempotency prevents a second meeting. + +### Meaningful completion + +The poll has exactly one authoritative outcome, one meeting is created, participants know the result, and external failures do not create ambiguous business state. + +--- + +## J-08: Cancel or reschedule a meeting + +**Current status:** Proposed +**Primary actors:** Host or authorized guest/participant +**Goal:** Change a commitment while keeping internal records and participant calendars consistent + +### Cancellation target journey + +```text +Authorized actor opens meeting + -> chooses Cancel + -> provides optional reason + -> confirms impact + -> system changes meeting state to CANCELLED + -> system records calendar and notification work + -> participants see the cancellation +``` + +### Direct rescheduling target journey + +```text +Authorized actor opens meeting + -> chooses Reschedule + -> selects a new valid time + -> system updates the meeting using the same calendar identity + -> participants receive an updated invitation +``` + +### Consensus rescheduling target journey + +```text +Host chooses Find a new time with attendees + -> system creates a rescheduling poll from the meeting + -> participants vote on new candidates + -> host finalizes replacement time + -> existing meeting is updated rather than duplicated +``` + +### Design questions + +- Which guests may cancel or reschedule without an account? +- Should public management use a revocable secret token? +- Does rescheduling create a new meeting version or update the original record? +- How should the original time remain visible in audit history? +- Which ICS sequence and status changes are required? + +### Meaningful completion + +All parties see one consistent meeting state, and no stale active calendar event remains after cancellation or rescheduling. + +--- + +## Cross-journey experience requirements + +### Clear state + +At every completion point, the UI should distinguish: + +- Business operation succeeded +- Secondary notification succeeded +- Secondary notification failed +- User action is still required + +### Timezone visibility + +Every displayed candidate or meeting time should identify the viewer's timezone and provide enough context to avoid accidental interpretation. + +### Authorization + +Every mutating server operation must revalidate identity, resource ownership or membership, resource lifecycle state, and submitted identifiers. A hidden button is not authorization. + +### Idempotency + +Repeated submission caused by refresh, retry, or network uncertainty must not create duplicate bookings, meetings, votes, or notifications. + +### Recoverability + +External provider failure should result in a visible, retryable state rather than a contradiction between UI and database truth. + +### Accessibility + +Journeys must remain operable with keyboard navigation, visible focus, semantic form labels, announced asynchronous status, and sufficient contrast. + +## Current journey gaps by priority + +| Priority | Gap | Affected journeys | +| --- | --- | --- | +| 1 | Poll cannot produce a finalized meeting | J-05, J-06, J-07 | +| 2 | Poll has no dashboard or lifecycle state | J-05, J-07 | +| 3 | Poll candidates are not multi-date and explicitly timezone-safe | J-05, J-06 | +| 4 | Booking cannot be cancelled or rescheduled | J-03, J-04, J-08 | +| 5 | First-time user has no guided setup | J-01, J-02 | +| 6 | Navigation and locale handling are inconsistent | All authenticated journeys | +| 7 | External calendar conflicts are not considered | J-02, J-03, J-07, J-08 | + +## How these journeys inform the next documents + +The next `use-cases.md` document will turn each important transition into precise behavior with: + +- Preconditions +- Trigger +- Main success flow +- Alternative and failure flows +- Authorization +- Postconditions +- Related business-rule identifiers + +The later domain model must support at least these facts: + +- A person can participate in different roles. +- A poll has a lifecycle and candidate times. +- A participant has an identity stronger than a free-form vote name when invited privately. +- A vote connects one participant to one candidate belonging to the same poll. +- Direct booking and poll finalization converge on a scheduled commitment. +- A meeting can change state and retain history. +- Notification and provider state are separate from meeting state. +- Timezone context and absolute timestamps serve different purposes. + From e61de8b992d2edf6a6ba4b0fc924306e78cd9645 Mon Sep 17 00:00:00 2001 From: Janarthanan Soundhararajan Date: Sat, 29 Aug 2026 23:43:35 +0530 Subject: [PATCH 2/2] docs: complete system design baseline --- docs/README.md | 14 +- ...02-deploy-nextjs-applications-on-vercel.md | 153 +++ docs/business-rules.md | 22 +- docs/content-opportunities.md | 98 +- docs/deployment-architecture.md | 386 ++++++++ docs/domain-decisions.md | 65 +- docs/domain-model-current.md | 17 +- docs/domain-model-proposed.md | 929 ++++++++++++++++++ docs/feature-inventory.md | 12 +- docs/roadmap.md | 389 ++++++++ docs/system-architecture.md | 452 +++++++++ docs/use-cases.md | 33 +- 12 files changed, 2469 insertions(+), 101 deletions(-) create mode 100644 docs/adr/0002-deploy-nextjs-applications-on-vercel.md create mode 100644 docs/deployment-architecture.md create mode 100644 docs/domain-model-proposed.md create mode 100644 docs/roadmap.md create mode 100644 docs/system-architecture.md diff --git a/docs/README.md b/docs/README.md index 6b49dbe..d5b2fc3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ This directory is the maintained source of truth for SlotSyncro's product and te SlotSyncro started as a portfolio project inspired by scheduling products such as Calendly and Doodle. Its proposed product direction is a timezone-aware scheduling and group-decision platform that supports direct booking and fairness-based consensus scheduling. +The core design-documentation baseline is complete as of 2026-08-29. Proposed-model and architecture checklists describe implementation evidence still to be produced; they do not mean the documentation itself is missing. + ## How to read these documents The documentation uses the following status labels: @@ -26,15 +28,16 @@ Statements about current behavior should be verifiable in code. Proposed behavio | [Feature inventory](./feature-inventory.md) | Current, partial, proposed, and later capabilities | Initial draft | | [Repository architecture](./repository-architecture.md) | Turborepo structure, application boundaries, dependency rules, and package strategy | Initial draft | | [ADR 0001: Modular monolith](./adr/0001-adopt-modular-monolith.md) | Accepted decision for structuring the product application as business modules | Accepted | +| [ADR 0002: Vercel deployment](./adr/0002-deploy-nextjs-applications-on-vercel.md) | Accepted host and project boundaries for the two Next.js applications | Accepted | | [User journeys](./user-journeys.md) | Actors, current experience, target outcomes, and cross-journey requirements | Initial draft | | [Use cases](./use-cases.md) | Preconditions, authorization, success, failures, postconditions, and preliminary rules | Initial draft | | [Business rules](./business-rules.md) | Authoritative invariants, current enforcement, target rules, and test implications | Initial draft | | [Current domain model](./domain-model-current.md) | Existing Prisma entities, ER diagram, keys, cardinality, constraints, indexes, and integrity gaps | Current-state analysis | -| [Proposed domain decisions](./domain-decisions.md) | Recommended choices and trade-offs that must be reviewed before the proposed ER model | Recommended for review | -| Proposed domain model | Target entities, aggregate boundaries, lifecycle states, constraints, and migration path | Planned | -| System architecture | Runtime components, integrations, security boundaries, and sequences | Planned | -| Deployment architecture | Environments, domains, infrastructure, and operational concerns | Planned | -| Roadmap | Vertical delivery milestones and dependencies | Planned | +| [Proposed domain decisions](./domain-decisions.md) | Accepted modeling choices, trade-offs, and explicitly unresolved mechanisms | Accepted directions | +| [Proposed domain model](./domain-model-proposed.md) | Target entities, ER diagrams, lifecycle states, constraints, transactions, and migration path | Proposed logical design | +| [System architecture](./system-architecture.md) | Modular runtime boundaries, request and transaction flows, durable work, integrations, security, and sequences | Proposed runtime design | +| [Deployment architecture](./deployment-architecture.md) | Environments, deployable units, database, releases, secrets, observability, backups, and recovery | Proposed deployment design | +| [Roadmap](./roadmap.md) | Vertical delivery milestones, dependencies, decision gates, and completion evidence | Proposed delivery plan | | [Content opportunities](./content-opportunities.md) | Reusable learning and publishing ideas discovered during development | Active backlog | ## Articles @@ -86,6 +89,7 @@ Small implementation details do not require ADRs. Accepted decisions: - [ADR 0001: Adopt a modular monolith for the product application](./adr/0001-adopt-modular-monolith.md) +- [ADR 0002: Deploy the Next.js applications on Vercel](./adr/0002-deploy-nextjs-applications-on-vercel.md) ## Existing historical context diff --git a/docs/adr/0002-deploy-nextjs-applications-on-vercel.md b/docs/adr/0002-deploy-nextjs-applications-on-vercel.md new file mode 100644 index 0000000..0ac4532 --- /dev/null +++ b/docs/adr/0002-deploy-nextjs-applications-on-vercel.md @@ -0,0 +1,153 @@ +# ADR 0002: Deploy the Next.js Applications on Vercel + +- **Status:** Accepted +- **Date:** 2026-08-29 +- **Decision owners:** SlotSyncro maintainers + +## Context + +SlotSyncro is a Turborepo containing two Next.js applications: + +- `apps/marketing`, the public marketing and educational surface +- `apps/app`, the scheduling product, Server Actions, route handlers, authentication, and public booking/poll experiences + +The applications should deploy independently while remaining in one repository. The deployment platform should support Next.js and Turborepo without requiring the project to operate container orchestration or custom application servers at its current stage. + +The product application also needs preview deployments, environment-specific configuration, OAuth callback URLs, provider webhooks, and server-side connectivity to managed PostgreSQL. Future notification, reminder, and calendar work requires durable asynchronous execution, but the exact worker mechanism has not yet been selected. + +## Decision + +Deploy both Next.js applications on Vercel as separate Vercel projects connected to the same Git repository: + +| Application | Vercel project root | Proposed production domain | +| --- | --- | --- | +| Marketing | `apps/marketing` | `slotsyncro.com` | +| Product | `apps/app` | `app.slotsyncro.com` | + +`packages/db` remains a shared workspace package consumed during build and at product runtime. It is not an independently deployed service. + +Managed PostgreSQL remains external to the application runtime. The current implementation uses Neon-compatible Prisma infrastructure. The database and product-function regions should be located close together where the selected plans permit region configuration. + +Vercel will initially own: + +- Next.js builds and application runtime +- Preview and production deployments +- Custom-domain routing and managed TLS +- Environment-scoped runtime configuration +- Product route-handler execution, including verified webhooks + +Vercel is not granted authority over internal domain truth. PostgreSQL remains authoritative for meetings, polls, notification intent, attempts, and integration state. + +## Background-work boundary + +This decision does not select Vercel Cron, Vercel Queues, or Vercel Workflow as the final durable-work mechanism. + +The application architecture must preserve portability: + +```text +Use case + -> commit business state and durable work intent + -> worker/dispatcher claims work + -> provider adapter performs delivery +``` + +Vercel Cron does not automatically retry a failed invocation and plan-specific scheduling limits affect its suitability for prompt delivery. Vercel Queues offers durable at-least-once delivery and retries but is currently documented as Beta. These mechanisms should be evaluated when durable notification delivery is implemented. + +Regardless of the trigger, consumers must remain idempotent and notification/provider attempts must be persisted independently of meeting state. + +## Configuration consequences + +- Marketing and product receive separate environment-variable sets. +- Marketing does not receive database, authentication, email, or calendar credentials without a reviewed use case. +- Preview and production use separate OAuth/provider configuration where callbacks or data isolation require it. +- Canonical URLs, cookies, OAuth callbacks, email links, and webhook registrations must agree with the environment's product domain. +- Secret values remain in Vercel environment configuration or an approved secret manager, never in repository files. + +## Build consequences + +- Each application is configured as a separate project with its own root directory. +- Builds may use Turborepo filtering so unrelated applications can skip deployment work. +- Prisma Client generation remains an upstream build requirement for the product application. +- Production database migrations run as a controlled, serialized release step rather than from every application instance at startup. +- Preview deployment success does not authorize preview code to use production data or provider credentials. + +## Alternatives considered + +### Self-managed virtual machine + +This provides maximum runtime control and supports long-lived workers, but requires operating reverse proxies, TLS, deploy automation, process supervision, scaling, and patching. That operational burden is not justified for the current application. + +### Container platform + +Platforms that run containers offer portability and straightforward long-running workers. They remain a future option, but introduce image, registry, networking, health, and scaling configuration before SlotSyncro has demonstrated those requirements. + +### Single Vercel project for both applications + +Rejected because the applications have different domains, release concerns, environment-variable needs, and runtime responsibilities. Separate projects make those boundaries visible while preserving one repository. + +### Different frontend and product hosts + +Possible, but it adds deployment-system inconsistency without a current need. A single platform is simpler while both surfaces use Next.js. + +## Consequences + +### Positive + +- Strong alignment with the current Next.js and Turborepo stack +- Independent marketing and product deployments +- Preview deployments that support portfolio review and change validation +- Low initial infrastructure-management overhead +- Managed scaling and TLS +- A clear path from repository boundaries to deployment boundaries + +### Costs and risks + +- Function execution and scheduled-work constraints influence background processing +- Usage-based pricing must be monitored as traffic grows +- Platform-specific configuration can increase switching cost +- Preview OAuth and provider callback configuration requires care +- Durable background-work products may change maturity or pricing +- Long-running or infrastructure-specialized workloads may later require another runtime + +## Portability rules + +To limit unnecessary platform coupling: + +1. Keep business rules independent of Vercel APIs. +2. Put Vercel request/trigger handling in delivery or infrastructure adapters. +3. Persist business state and work intent in PostgreSQL before provider delivery. +4. Keep provider consumers idempotent and callable through narrow interfaces. +5. Avoid treating ephemeral filesystem or process memory as durable state. +6. Record any deeper Vercel-specific commitment in a separate ADR. + +## Validation + +The decision is successful when: + +- Both applications deploy independently from the monorepo. +- A change isolated to one application can avoid unnecessary work for the other. +- Preview environments cannot mutate production data. +- Product runtime can connect safely to PostgreSQL without exhausting connections. +- OAuth, public links, and verified webhooks use correct environment URLs. +- A failed email/calendar provider call does not undo committed meeting state. +- Deployment and provider usage are observable enough to identify cost or reliability risks. + +## Revisit conditions + +Revisit the hosting decision when measured evidence shows a need for: + +- Long-lived or specialized compute not suited to the selected Vercel runtime +- More precise or higher-volume background processing than the selected mechanism supports +- Infrastructure controls, data residency, or networking unavailable on the selected plan +- Cost behavior that is materially worse than a suitable alternative +- Independent reliability or scaling requirements for a module + +The existence of more features alone is not a reason to migrate. + +## References + +- [Vercel monorepo documentation](https://vercel.com/docs/monorepos) +- [Deploying Turborepo to Vercel](https://vercel.com/docs/monorepos/turborepo) +- [Vercel Fluid Compute](https://vercel.com/docs/fluid-compute) +- [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) +- [Vercel Queues](https://vercel.com/docs/queues) diff --git a/docs/business-rules.md b/docs/business-rules.md index bdb18e1..624f8f7 100644 --- a/docs/business-rules.md +++ b/docs/business-rules.md @@ -298,20 +298,14 @@ When a business rule changes: 5. Update tests that reference the rule. 6. Record an ADR if the change is long-lived, cross-cutting, or expensive to reverse. -## Next documentation step +## Related design outcomes -Create the **current domain model and ER diagram** directly from the existing Prisma schema. +The schema audit and subsequent design are now documented in: -The current-model document should identify: - -- Entities and their present names -- Primary and foreign keys -- Cardinality and optionality -- Unique constraints and indexes -- Referential actions -- Rules currently enforced by schema -- Rules that the schema cannot currently enforce -- Naming collisions such as `Availability` meaning a poll vote while `UserAvailability` means a schedule - -Only after the current model is accurate should a separate proposed ER model be designed. +- [Current domain model](./domain-model-current.md) +- [Proposed domain decisions](./domain-decisions.md) +- [Proposed domain model](./domain-model-proposed.md) +- [System architecture](./system-architecture.md) +- [Roadmap](./roadmap.md) +Business rules remain authoritative inputs. As implementation progresses, update each rule's status and enforcement evidence rather than treating the proposed model as already shipped. diff --git a/docs/content-opportunities.md b/docs/content-opportunities.md index 321aa70..447838b 100644 --- a/docs/content-opportunities.md +++ b/docs/content-opportunities.md @@ -198,7 +198,103 @@ Possible formats: - **LinkedIn:** Explain provenance versus current business state - **X:** Keep it general: distinguish an order from the checkout session, a shipment from its purchase flow, or a meeting from its scheduling method -Readiness: **Ready after DD-001 and DD-002 are accepted and represented in the proposed ER model.** +Readiness: **Ready for an outline; DD-001 and DD-002 are accepted and represented in the proposed ER model.** + +### 11. Design the migration before writing the target schema + +Core lesson: + +> A proposed ER diagram is incomplete until every current entity has a destination and risky data has a validation strategy. + +Possible formats: + +- **Article:** Current schema to target model without a clean-slate rewrite +- **YouTube:** Walk through additive migration, backfill, cutover, constraints, and legacy removal +- **Short/Reel:** “Your target schema is the easy part; preserving existing truth is the design” +- **LinkedIn:** Seven-phase migration map from user-owned scheduling data to meeting/workspace aggregates +- **X:** Keep it general: explain why migrations need data-quality reports before stricter constraints + +Readiness: **Ready for an outline; implementation evidence should wait for the first migration phase.** + +### 12. Commit business truth before calling external systems + +Core lesson: + +> A successful database transaction and a successful email or calendar request are different outcomes with different recovery strategies. + +Possible formats: + +- **Article:** Design reliable side effects in a modular monolith without premature microservices +- **YouTube:** Trace booking confirmation from transaction to durable notification attempt +- **Short/Reel:** “An email timeout should not unbook a confirmed meeting” +- **LinkedIn:** Compare network calls inside a transaction with durable post-commit work +- **X:** Keep it general: “Commit business truth first. Record the work it requires in the same transaction. Let retryable workers handle unreliable networks.” + +Readiness: **Architecture explanation is ready; implementation evidence should follow the first durable notification slice.** + +### 13. Authentication is identity; authorization is a resource decision + +Core lesson: + +> Knowing who made a request does not prove they can modify the referenced record. + +Possible formats: + +- **Article:** Model authenticated, guest-token, and system actors in one application +- **YouTube:** Threat-model a public booking and invitation-only poll flow +- **Short/Reel:** “A valid session is not ownership” +- **LinkedIn:** Explain why authorization belongs inside the use case, not only in UI visibility +- **X:** Keep it general: “Authentication answers who you are. Authorization answers whether you may perform this operation on this resource—using its current state.” + +Readiness: **Ready for an outline; token implementation should be demonstrated only after security tests exist.** + +### 14. A production architecture is a set of recovery guarantees + +Core lesson: + +> A deployment diagram is incomplete if it shows where software runs but not how changes, failures, backups, and retries are handled. + +Possible formats: + +- **Article:** Turn a monorepo architecture diagram into a production readiness plan +- **YouTube:** Design environments, migrations, workers, observability, and recovery for a small product +- **Short/Reel:** “A backup you have never restored is an assumption” +- **LinkedIn:** Explain why RTO, RPO, migration compatibility, and durable work belong in architecture documentation +- **X:** Keep it general: “Production-ready is not a cloud-provider logo. It is knowing how you deploy, detect failure, preserve data, retry work, and recover.” + +Readiness: **Documentation lesson is ready; operational claims should wait until a deployment and restore exercise exist.** + +### 15. Choose a platform without coupling the domain to it + +Core lesson: + +> Platform-native delivery can reduce operations while ports, durable state, and explicit boundaries preserve the option to move specialized workloads later. + +Possible formats: + +- **Article:** Deploy a modular monolith to a serverless platform without making the domain serverless-specific +- **YouTube:** Map two monorepo applications to independent deployments and shared infrastructure +- **Short/Reel:** “Using a platform is not the same as putting it inside your business logic” +- **LinkedIn:** Explain the difference between accepting a host and delegating every architecture decision to it +- **X:** Keep it general: “Portability does not require avoiding platform features. Keep business truth portable; isolate platform triggers and adapters at the edges.” + +Readiness: **ADR and architecture explanation are ready; deployment walkthrough should wait for an actual preview release.** + +### 16. A roadmap should sequence risk, not just features + +Core lesson: + +> The most useful engineering roadmap connects user outcomes to data migrations, decision gates, failure risks, and proof of completion. + +Possible formats: + +- **Article:** Turn product documentation into a vertical architecture roadmap +- **YouTube:** Prioritize a scheduling product from UX coherence through transactions and recovery +- **Short/Reel:** “A milestone is not done because the table exists” +- **LinkedIn:** Compare a feature checklist with outcome, risk, dependency, and evidence-driven milestones +- **X:** Keep it general: “Sequence roadmaps by irreversible risk: clarify the user outcome, settle invariants, plan migration, prove concurrency, then add integrations.” + +Readiness: **Ready from the documentation case study; implementation retrospectives can strengthen it later.** ## Publishing record template diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md new file mode 100644 index 0000000..9ed8889 --- /dev/null +++ b/docs/deployment-architecture.md @@ -0,0 +1,386 @@ +# Proposed Deployment Architecture + +**Document status:** Proposed deployment design with accepted application host + +**Scope:** Environments, deployable units, infrastructure, delivery, operations, and recovery + +**Related documents:** [Repository architecture](./repository-architecture.md), [system architecture](./system-architecture.md), [ADR 0002](./adr/0002-deploy-nextjs-applications-on-vercel.md) + +## Purpose + +This document maps the logical system architecture to deployable runtime components. It distinguishes the repository's current local setup from a production-minded target so infrastructure is introduced only when a product capability requires it. + +Vercel is accepted as the host for both Next.js applications. Database, background-work, and observability vendor choices remain independently evaluated so application hosting does not silently determine every infrastructure decision. + +## Current deployment readiness + +The repository currently contains: + +- `apps/marketing`, a Next.js application that runs locally on port 3000 +- `apps/app`, a Next.js product application that runs locally on port 3001 +- `packages/db`, a shared Prisma 7 and Neon/PostgreSQL adapter package +- Turborepo tasks for development, build, lint, Prisma generation, and development schema push +- OAuth configuration for Google and GitHub in the product application + +Vercel is the accepted production host, but the repository does not yet contain its project configuration or a deployed production environment. It also does not yet define a complete CI/CD pipeline, background-worker runtime, infrastructure-as-code configuration, formal migration pipeline, or recovery procedure. Those items are proposed below, not currently operational. + +## Environment model + +Use three long-lived environment classes: + +| Environment | Purpose | Data policy | +| --- | --- | --- | +| Local | Developer feedback and isolated experimentation | Synthetic or explicitly safe development data | +| Preview | Per-change review of integrated UI and server behavior | Isolated or sanitized data; never silently share production writes | +| Production | Real user traffic and durable business state | Restricted access, monitored changes, backups, and recovery controls | + +A shared staging environment may be added when persistent provider certification, migration rehearsal, or cross-team testing justifies its cost. Preview deployments should not automatically receive production OAuth credentials, email domains, calendar tokens, or databases. + +## Deployable units + +The target retains a small operational footprint: + +```mermaid +flowchart TB + Internet[Internet] + DNS[DNS and TLS] + Marketing[Marketing Vercel project] + Product[Product Vercel project] + Worker[Background worker or scheduled dispatcher] + DB[(Managed PostgreSQL)] + Email[Email provider] + Calendar[Calendar and conferencing providers] + Observe[Logs, metrics, alerts, error tracking] + + Internet --> DNS + DNS --> Marketing + DNS --> Product + Product --> DB + Worker --> DB + Worker --> Email + Worker --> Calendar + Email -. webhook .-> Product + Calendar -. webhook .-> Product + Marketing --> Observe + Product --> Observe + Worker --> Observe +``` + +### Marketing application + +Suggested public address: `slotsyncro.com`. + +Responsibilities: + +- Public product and educational pages +- Search-indexed content +- Legal and trust information +- Calls to action into the product + +It should deploy independently and must not receive database, authentication, email, or calendar secrets unless a reviewed server-side use case later requires them. + +Configure it as a Vercel project rooted at `apps/marketing`. + +### Product application + +Suggested public address: `app.slotsyncro.com`. + +Responsibilities: + +- Authenticated dashboard +- Public booking and poll pages +- Server Actions and route handlers +- Authentication callbacks +- Verified provider webhooks +- Synchronous modular-monolith use cases + +The product is one deployable application even though its source is divided into business modules. + +Configure it as a separate Vercel project rooted at `apps/app`. `packages/db` remains a workspace dependency rather than a third deployment. + +### Background worker + +The worker handles durable work such as notification delivery, retries, calendar synchronization, and later reminders. It can initially share the product application's codebase and deployment artifact while running through a scheduled or queue-triggered entry point. + +A separately managed service is not required initially. Vercel Cron, Queues, and Workflow are candidates, but the mechanism remains deferred. Extract work into an independently deployed process only when execution limits, throughput, isolation, or scheduling requirements provide evidence for doing so. + +The worker must: + +- Atomically claim eligible work +- Use a lease or recoverable processing state +- Apply bounded retries with backoff +- Record every delivery attempt +- Be safe when multiple instances run concurrently +- Expose terminal failures for investigation + +## Domain and routing model + +The proposed public routing boundary is: + +```text +slotsyncro.com -> apps/marketing +app.slotsyncro.com -> apps/app +``` + +Both domains use Vercel-managed routing and TLS with explicit redirect/canonical-host rules. OAuth callback URLs, trusted origins, cookie configuration, email links, and provider webhooks must use the environment's canonical product URL. + +Preview URLs require separate OAuth applications or a controlled callback strategy. Wildcard callbacks should not be assumed because providers differ and broad callback rules increase risk. + +## Database topology + +Use managed PostgreSQL as the authoritative store. The application and worker may share one database because they participate in one modular-monolith consistency boundary. + +```mermaid +flowchart LR + Product[Product runtime] --> Pool[Managed connection/pooling layer] + Worker[Worker runtime] --> Pool + Migration[Controlled migration job] --> Direct[Migration-safe connection] + Pool --> Primary[(PostgreSQL primary)] + Direct --> Primary + Primary -. managed backup/PITR .-> Recovery[Recovery storage] +``` + +Operational requirements: + +- Separate databases or isolated branches/projects per environment +- TLS connections and least-privilege credentials +- Connection limits compatible with horizontally scaled/serverless runtimes +- A dedicated migration path where provider pooling restrictions require it +- Automated backups and point-in-time recovery when real user data exists +- Restore testing, because an untested backup is only an assumption + +Read replicas are unnecessary until measured read load or reporting isolation justifies them. + +## Build and release pipeline + +Turborepo should preserve dependency-aware builds while each application remains independently deployable. + +```mermaid +flowchart LR + Change[Pull request] --> Install[Locked dependency install] + Install --> Generate[Prisma generate] + Generate --> Quality[Lint, type checks, tests] + Quality --> Build[Build affected applications] + Build --> Preview[Preview deployment] + Preview --> Review[Automated and manual checks] + Review --> Merge[Merge approved change] + Merge --> Migrate[Controlled production migration] + Migrate --> Deploy[Production deployment] + Deploy --> Verify[Health and journey verification] +``` + +Minimum release gates: + +- Lockfile-respecting dependency installation +- Prisma client generation +- Lint and type checking +- Unit and application-service tests +- PostgreSQL integration tests for migrations and constraints +- Production builds for affected workspaces +- Migration safety review when schema changes exist +- Post-deployment smoke tests for critical journeys + +Remote build caching may be introduced after CI secret handling and cache trust boundaries are defined. Database mutation tasks remain uncached. + +## Database migrations + +Production schema changes use committed Prisma migrations, not `db push`. `db push` remains a local prototyping tool. + +Use an expand-and-contract approach: + +1. **Expand:** add compatible tables, columns, constraints, or indexes. +2. **Deploy compatible code:** read old state while writing the new representation where necessary. +3. **Backfill:** migrate existing records with measured counts and resumable batches. +4. **Verify:** compare old/new representations and inspect invalid data. +5. **Cut over:** make new reads authoritative. +6. **Constrain:** add stricter non-null, unique, or relational guarantees after data is valid. +7. **Contract:** remove legacy structures in a later release. + +Migration execution must be serialized. Application instances should not each attempt production migrations during startup. + +Every risky migration needs: + +- Expected lock and runtime characteristics +- Pre-migration validation queries +- Backup/recovery posture +- Forward-fix or rollback plan +- Post-migration reconciliation counts + +## Configuration and secrets + +Configuration is environment-specific and supplied by the deployment platform or secret manager. Commit variable names and purpose, never secret values. + +Currently verified server-side variables include: + +| Variable | Consumer | Purpose | +| --- | --- | --- | +| `DATABASE_URL` | Product runtime, worker, Prisma tooling | PostgreSQL connection | +| `AUTH_GOOGLE_ID` | Product application | Google OAuth client identity | +| `AUTH_GOOGLE_SECRET` | Product application | Google OAuth client secret | +| `AUTH_GITHUB_ID` | Product application | GitHub OAuth client identity | +| `AUTH_GITHUB_SECRET` | Product application | GitHub OAuth client secret | + +Authentication framework secrets, canonical URL settings, email credentials, webhook-signing secrets, calendar credentials, encryption keys, and worker-trigger credentials must be documented when their exact runtime contracts are reconciled with the implementation branch. + +Secret rules: + +- Scope each secret to only the deployment that consumes it. +- Use separate credentials per environment. +- Keep secrets out of browser-exposed variables, logs, build output, fixtures, and preview comments. +- Rotate credentials without requiring data migration where possible. +- Treat provider OAuth refresh tokens as sensitive persisted secrets and encrypt them at rest. +- Audit access to production secrets. + +## Email and calendar delivery + +Only the worker should normally perform retryable provider delivery. Product requests commit notification or integration work and return based on internal business state. + +Production email readiness requires: + +- A verified sending domain +- SPF, DKIM, and appropriate DMARC policy +- Environment-specific recipients or suppression in non-production +- Provider event/webhook verification +- Bounce, complaint, and suppression handling +- Stable message idempotency keys +- No sensitive values in provider metadata + +Calendar readiness requires: + +- Environment-specific OAuth applications +- Minimal provider scopes +- Encrypted refresh-token storage +- Idempotent external event creation/update +- Webhook renewal and deduplication +- Reconciliation for missed or out-of-order provider events + +## Observability and alerting + +Logs, metrics, traces/error reports, and domain history answer different questions and should remain distinct. + +### Structured logs + +Include: + +- Environment and deploy version +- Correlation/request ID +- Use-case or job name +- Safe internal resource IDs +- Outcome and stable error category +- Duration and retry attempt + +Exclude secrets, raw tokens, database URLs, OAuth payloads, and unnecessary personal data. + +### Initial service indicators + +Track: + +- Product request error rate and latency +- Booking and poll-finalization success/conflict rates +- Database connection saturation and query latency +- Pending-work count and age of oldest item +- Notification/calendar attempt success and terminal failure rates +- Webhook invalid-signature and duplicate counts +- Deployment health and critical-journey smoke results + +Alerts should identify an actionable user or data risk. Avoid alerting on every isolated provider retry. + +## Health and readiness + +The product runtime needs a lightweight liveness signal and, where the platform supports it, a readiness signal that reflects whether it can safely serve requests. + +Do not make a health endpoint perform expensive provider calls. Provider degradation should appear in delivery metrics and work-item state rather than taking the entire product offline. + +Worker health is better measured by progress—claim success, oldest-work age, and terminal failures—than by the existence of a running process alone. + +## Backup and recovery + +Before storing important user data, define: + +- Automated backup frequency and retention +- Point-in-time recovery window +- Recovery-time objective (RTO): acceptable restoration duration +- Recovery-point objective (RPO): acceptable amount of data loss measured in time +- Restore ownership and access +- A recurring restore test + +Database backup does not automatically restore provider state. Recovery procedures must reconcile internal meetings, notification attempts, and external calendar events after a restore. + +Prefer forward fixes for ordinary application releases. Rollback is safe only when the previous application version remains compatible with the migrated schema. + +## Security and operational access + +- Production database and provider dashboards use least-privilege access and multi-factor authentication. +- Deployment tokens belong to automation identities, not personal credentials. +- Production access is logged and limited to necessary maintainers. +- Public endpoints receive rate limiting and abuse monitoring. +- Webhooks require signature verification and replay protection. +- Dependency and secret scanning should run in CI when the pipeline is introduced. +- Security headers and cookie behavior must be verified independently for marketing and product domains. + +## Cost and scaling principles + +Scale from evidence: + +- Increase product instances for measured request concurrency. +- Increase worker concurrency for measured queue age, while respecting database and provider limits. +- Add caching for demonstrated read patterns with clear invalidation rules. +- Add read replicas only for measured database pressure. +- Split services only for independent scaling, reliability, security, deployment, data, or team ownership needs. + +Feature count alone does not justify distributed infrastructure. + +## Failure scenarios + +| Failure | Expected behavior | +| --- | --- | +| Email provider unavailable | Meeting stays confirmed; notification retries and becomes visible if terminal | +| Calendar provider timeout | Internal meeting remains authoritative; synchronization retries idempotently | +| Worker stops | Pending work accumulates durably; alert on age; resume without losing intent | +| Product deploy fails | Previous healthy release continues or deployment rolls back without schema incompatibility | +| Migration fails | Stop rollout, preserve logs/counts, apply reviewed recovery or forward fix | +| Database unavailable | Reject state changes safely; do not claim success; recover through managed service procedures | +| Duplicate webhook | Deduplication returns success without repeating the transition | +| Database restore | Reconcile durable work and external provider state before declaring full recovery | + +## Incremental implementation plan + +1. Create separate Vercel projects rooted at `apps/marketing` and `apps/app`. +2. Configure preview and production domains, environment variables, and deployment protection. +3. Provision isolated preview and production databases. +4. Replace production `db push` assumptions with committed migration execution. +5. Add structured logging, deploy version, and critical-journey smoke checks. +6. Introduce notification work items and a scheduled dispatcher. +7. Add provider webhook verification and delivery monitoring. +8. Enable production backup/PITR and run a documented restore exercise. +9. Add calendar worker capabilities only after internal meeting lifecycle is stable. + +## Deferred decisions + +- CI/CD checks beyond Vercel's Git deployment integration +- Worker execution model: scheduled endpoint, queue consumer, or long-lived process +- Preview database isolation mechanism +- Observability and error-tracking vendors +- Exact backup retention, RTO, and RPO +- Rate-limit technology and thresholds +- Custom-domain support +- Infrastructure-as-code tool and adoption point + +These decisions should be made near implementation, with current provider constraints and measured requirements. + +## Acceptance checklist + +- [ ] Current and proposed deployment capabilities are clearly separated. +- [ ] Marketing, product, worker, database, and provider trust boundaries are explicit. +- [ ] Every environment has isolated credentials and an intentional data policy. +- [ ] Production schema changes use reviewed, serialized migrations. +- [ ] Application deployment and schema evolution remain backward compatible during rollout. +- [ ] Durable external work can retry without duplicating business outcomes. +- [ ] Secrets are scoped, rotatable, and absent from client/build output. +- [ ] Logs and metrics can trace requests, jobs, deploys, and provider failures safely. +- [ ] Backups have defined recovery targets and a tested restore procedure. +- [ ] Scaling decisions depend on evidence rather than portfolio appearance. + +## Related delivery plan + +The [roadmap](./roadmap.md) converts this architecture into vertical milestones with dependencies, decision gates, tests, migrations, operational evidence, and content checkpoints. diff --git a/docs/domain-decisions.md b/docs/domain-decisions.md index 71f6522..0b97d43 100644 --- a/docs/domain-decisions.md +++ b/docs/domain-decisions.md @@ -1,6 +1,6 @@ # Proposed Domain Decisions -**Document status:** Recommended for review +**Document status:** Accepted modeling directions; explicitly noted mechanisms remain unresolved **Purpose:** Resolve the major choices exposed by the journeys, use cases, business rules, and current ER audit before drawing the proposed ER model ## How to use this document @@ -31,24 +31,24 @@ Accepted decisions that are expensive to reverse will later receive their own AD | ID | Decision | Recommendation | Status | | --- | --- | --- | --- | -| DD-001 | Scheduled commitment | Introduce `Meeting` as the shared final commitment | Recommended | -| DD-002 | Direct-booking source | Retain a source record linked one-to-one with its meeting | Recommended | -| DD-003 | Poll outcome | Finalized poll links to one selected candidate and one meeting | Recommended | -| DD-004 | Participant identity | Use poll/meeting-scoped participant entities with optional user link | Recommended | -| DD-005 | Ownership boundary | Introduce a personal workspace for every user; add team UX later | Recommended | -| DD-006 | Availability model | Separate schedule timezone from user profile and normalize windows | Recommended | -| DD-007 | Lifecycle history | Current state plus append-only lifecycle events; not full event sourcing | Recommended | -| DD-008 | Deletion and retention | Archive configuration; preserve/anonymize transactional history | Recommended | -| DD-009 | Notifications | Persist notification intent and delivery attempts separately | Recommended | -| DD-010 | External integrations | Keep provider connections/references outside core meeting columns | Recommended | -| DD-011 | Concurrency | Database-backed overlap protection plus command idempotency | Recommended, mechanism unresolved | -| DD-012 | Open and private polls | Support both through explicit access policy | Recommended | +| DD-001 | Scheduled commitment | Introduce `Meeting` as the shared final commitment | Accepted | +| DD-002 | Direct-booking source | Retain a source record linked one-to-one with its meeting | Accepted | +| DD-003 | Poll outcome | Finalized poll links to one selected candidate and one meeting | Accepted | +| DD-004 | Participant identity | Use poll/meeting-scoped participant entities with optional user link | Accepted | +| DD-005 | Ownership boundary | Introduce a personal workspace for every user; add team UX later | Accepted | +| DD-006 | Availability model | Separate schedule timezone from user profile and normalize windows | Accepted | +| DD-007 | Lifecycle history | Current state plus append-only lifecycle events; not full event sourcing | Accepted | +| DD-008 | Deletion and retention | Archive configuration; preserve/anonymize transactional history | Accepted | +| DD-009 | Notifications | Persist notification intent and delivery attempts separately | Accepted | +| DD-010 | External integrations | Keep provider connections/references outside core meeting columns | Accepted | +| DD-011 | Concurrency | Database-backed overlap protection plus command idempotency | Accepted principle; mechanism unresolved | +| DD-012 | Open and private polls | Support both through explicit access policy | Accepted | --- ## DD-001: Introduce Meeting as the shared scheduled commitment -**Status:** Recommended +**Status:** Accepted ### Problem @@ -107,7 +107,7 @@ Preserves source-specific models but duplicates cancellation, rescheduling, part ## DD-002: Preserve a direct-booking source record -**Status:** Recommended +**Status:** Accepted ### Problem @@ -150,7 +150,7 @@ Store enough accepted booking context to interpret the meeting even if the event ## DD-003: Finalized poll has one selected candidate and one meeting -**Status:** Recommended +**Status:** Accepted ### Problem @@ -191,7 +191,7 @@ Whether an authorized host may explicitly reopen a finalized poll is deferred. D ## DD-004: Use scoped participant entities -**Status:** Recommended +**Status:** Accepted ### Problem @@ -246,7 +246,7 @@ The exact token design, response anonymity, and email-normalization policy remai ## DD-005: Introduce personal workspaces as the ownership boundary -**Status:** Recommended +**Status:** Accepted ### Problem @@ -312,7 +312,7 @@ Rejects multi-workspace membership and cannot represent different roles in diffe ## DD-006: Normalize recurring availability windows -**Status:** Recommended +**Status:** Accepted ### Problem @@ -366,7 +366,7 @@ The physical representation of local time—database `time`, minute-of-day integ ## DD-007: Current state plus append-only lifecycle events -**Status:** Recommended +**Status:** Accepted ### Problem @@ -417,7 +417,7 @@ This is an audit/history pattern, not full event sourcing. Current state remains ## DD-008: Archive configuration and preserve transactional history -**Status:** Recommended +**Status:** Accepted ### Problem @@ -447,7 +447,7 @@ Formal data-retention periods and legal/privacy obligations are outside the curr ## DD-009: Persist notification intent and attempts separately -**Status:** Recommended +**Status:** Accepted ### Problem @@ -487,7 +487,7 @@ Whether `Notification` itself acts as an outbox job or references a generic job ## DD-010: Isolate provider-specific integration data -**Status:** Recommended +**Status:** Accepted ### Problem @@ -527,7 +527,7 @@ Meeting location may use provider-neutral types such as video, phone, in-person, ## DD-011: Use database-backed overlap protection and command idempotency -**Status:** Recommended; exact mechanism unresolved +**Status:** Accepted principle; exact mechanism unresolved ### Problem @@ -568,7 +568,7 @@ The exact overlap mechanism is expensive to reverse and should receive an ADR af ## DD-012: Support open-link and invitation-only polls explicitly -**Status:** Recommended +**Status:** Accepted ### Problem @@ -651,17 +651,6 @@ Before accepting these recommendations, verify: - [ ] Concurrency and idempotency solve different invariants. - [ ] Migration from every current table is explainable. -## Next documentation step - -After reviewing or accepting these decisions, create `domain-model-proposed.md` with: - -- Entity definitions -- ER diagram -- Keys and cardinalities -- Lifecycle enums and transition diagrams -- Unique and check constraints -- Aggregate transaction boundaries -- Index/access-pattern analysis -- Mapping from current tables to proposed tables -- Incremental migration phases +## Related design outcomes +These decisions are represented in the [proposed domain model](./domain-model-proposed.md), realized at runtime by the [system architecture](./system-architecture.md), and sequenced in the [roadmap](./roadmap.md). Deferred physical mechanisms become focused ADRs or prototypes at the roadmap decision gates. diff --git a/docs/domain-model-current.md b/docs/domain-model-current.md index b73a317..a478416 100644 --- a/docs/domain-model-current.md +++ b/docs/domain-model-current.md @@ -777,7 +777,7 @@ The database does not currently guarantee: Some of these should remain application rules; others need database support in the proposed model. -## Questions for the proposed model +## Questions answered by the proposed model 1. Should `Booking` remain the final scheduled entity, or should direct booking create a `Meeting`? 2. Should historical meetings survive deletion of users and event-type configuration? @@ -790,17 +790,6 @@ Some of these should remain application rules; others need database support in t 9. How should workspace ownership coexist with host identity? 10. Which external provider fields belong in core tables versus integration-reference tables? -## Next documentation step +## Related target design -Create `domain-model-proposed.md` only after reviewing the questions above. - -The proposed model should: - -- Name aggregate roots explicitly. -- Separate identity, ownership, participation, and authorization. -- Make invalid cross-poll relationships structurally difficult or impossible. -- Define direct-booking and poll-finalization convergence. -- Represent lifecycle and history deliberately. -- Preserve internal truth independently from external provider state. -- Identify database constraints, application rules, and transaction boundaries. -- Include a migration path from every current entity rather than presenting a clean-slate schema only. +The answers and migration direction are recorded in the [proposed domain decisions](./domain-decisions.md) and [proposed domain model](./domain-model-proposed.md). This document remains the current-schema baseline until implementation changes the Prisma schema; update it after each migrated vertical slice. diff --git a/docs/domain-model-proposed.md b/docs/domain-model-proposed.md new file mode 100644 index 0000000..0a82e51 --- /dev/null +++ b/docs/domain-model-proposed.md @@ -0,0 +1,929 @@ +# Proposed Domain and ER Model + +**Document status:** Proposed logical design + +**Inputs:** Accepted domain directions, user journeys, use cases, business rules, and current-schema audit + +**Implementation status:** Not implemented + +## Purpose + +This document proposes a target relational model for SlotSyncro. It is a logical design used to evaluate domain boundaries, relationships, invariants, transactions, queries, and migration work before editing the Prisma schema. + +It is not: + +- A claim that these tables already exist +- A final Prisma schema +- Authorization implemented by foreign keys +- A clean-slate rewrite plan +- Permission to migrate production data without validation and rollback planning + +## Design goals + +The proposed model should: + +1. Let direct booking and poll finalization produce one shared meeting lifecycle. +2. Support authenticated and accountless participants without using display names as identity. +3. Establish a workspace ownership boundary while keeping individual UX simple. +4. Make invalid cross-poll votes and finalization references structurally difficult or impossible. +5. Separate recurring local-time rules from absolute scheduled instants. +6. Preserve historical meaning after configuration changes. +7. Keep notification and provider state independent from meeting truth. +8. Support cancellation, rescheduling, audit history, retries, and concurrency. +9. Remain one modular-monolith application with one PostgreSQL database. +10. Provide an incremental migration path from every current product table. + +## Accepted modeling decisions + +The recommendations in [Proposed domain decisions](./domain-decisions.md) are accepted as inputs to this model, with these qualifications: + +- The exact PostgreSQL mechanism for overlap prevention remains unresolved pending an implementation experiment. +- Token hashing, credential encryption, retention periods, and job claiming require detailed system/security design. +- Physical time-of-day representation will be confirmed against Prisma and PostgreSQL behavior. +- Team-workspace UX remains later work even though workspace ownership is modeled now. + +## Bounded-context overview + +```mermaid +flowchart LR + Identity[Identity] + Workspace[Workspace and membership] + Scheduling[Scheduling configuration] + Polling[Polling and consensus] + Meeting[Meeting lifecycle] + Notification[Notifications] + Integration[External integrations] + + Identity --> Workspace + Workspace --> Scheduling + Workspace --> Polling + Workspace --> Meeting + Scheduling --> Meeting + Polling --> Meeting + Meeting --> Notification + Meeting --> Integration +``` + +These are module and aggregate ownership boundaries, not microservices. + +## Proposed entity catalogue + +| Context | Entity | Purpose | +| --- | --- | --- | +| Identity | `User` | Authenticated person and profile identity | +| Identity | `Account`, `Session`, `VerificationToken` | Auth.js infrastructure | +| Workspace | `Workspace` | Personal or team tenant boundary | +| Workspace | `WorkspaceMembership` | User role inside one workspace | +| Workspace | `WorkspaceInvitation` | Pending membership invitation | +| Scheduling | `AvailabilitySchedule` | Named recurring schedule with its own timezone | +| Scheduling | `AvailabilityWindow` | One local weekday time range | +| Scheduling | `EventType` | Reusable direct-booking configuration | +| Polling | `Poll` | Group scheduling decision process | +| Polling | `PollCandidate` | One proposed absolute interval | +| Polling | `PollParticipant` | Poll-scoped authenticated or accountless person | +| Polling | `PollPreference` | Participant preference for one candidate | +| Polling | `PollFinalization` | One authoritative poll decision and resulting meeting | +| Meeting | `Meeting` | Final scheduled commitment and lifecycle root | +| Meeting | `DirectBooking` | Direct-booking source and accepted guest submission | +| Meeting | `MeetingParticipant` | Meeting-scoped host or attendee snapshot | +| Meeting | `MeetingEvent` | Append-only important lifecycle transition | +| Notification | `Notification` | Durable intent to communicate with one recipient | +| Notification | `NotificationAttempt` | One provider delivery attempt | +| Integration | `CalendarConnection` | Authorized external calendar account connection | +| Integration | `ExternalCalendarEvent` | Provider event synchronized with one meeting | + +## Identity and workspace ER model + +```mermaid +erDiagram + USER ||--o{ ACCOUNT : links + USER ||--o{ SESSION : may_have + USER ||--o{ WORKSPACE_MEMBERSHIP : joins + WORKSPACE ||--o{ WORKSPACE_MEMBERSHIP : contains + WORKSPACE ||--o{ WORKSPACE_INVITATION : issues + USER o|--o{ WORKSPACE_INVITATION : sends + USER o|--o| WORKSPACE : personally_owns + + USER { + string id PK + string email UK + string username UK + string profileTimeZone + datetime onboardingCompletedAt + } + + WORKSPACE { + string id PK + string slug UK + enum type + string personalOwnerUserId UK + datetime archivedAt + } + + WORKSPACE_MEMBERSHIP { + string id PK + string workspaceId FK + string userId FK + enum role + enum status + datetime joinedAt + } + + WORKSPACE_INVITATION { + string id PK + string workspaceId FK + string email + enum role + string tokenHash UK + datetime expiresAt + datetime acceptedAt + } +``` + +### Workspace invariants + +- Every product resource belongs to one workspace. +- A personal workspace has exactly one `personalOwnerUserId`. +- A user has at most one personal workspace. +- Every personal workspace owner has an active `OWNER` membership. +- A user has at most one effective membership per workspace. +- Membership role belongs to the user-workspace relationship, not to `User` globally. +- Team workspaces have no `personalOwnerUserId`; ownership is represented through membership roles. +- Archived workspaces reject new product mutations according to policy. + +### Key constraints + +| Entity | Constraint | Purpose | +| --- | --- | --- | +| Workspace | Unique `slug` | Workspace route identity | +| Workspace | Unique nullable `personalOwnerUserId` | One personal workspace per user | +| WorkspaceMembership | Unique `(workspaceId, userId)` | One membership per user/workspace | +| WorkspaceInvitation | Unique `tokenHash` | One verifiable invitation token | +| WorkspaceInvitation | Candidate unique active `(workspaceId, normalizedEmail)` | Avoid duplicate active invitations; exact conditional strategy deferred | + +### Authorization boundary + +Foreign keys prove membership rows exist. Application policy must still evaluate: + +- Membership status +- Required role or permission +- Workspace archived state +- Resource ownership +- Action-specific delegation + +## Scheduling-configuration ER model + +```mermaid +erDiagram + WORKSPACE ||--o{ AVAILABILITY_SCHEDULE : owns + USER ||--o{ AVAILABILITY_SCHEDULE : uses + AVAILABILITY_SCHEDULE ||--o{ AVAILABILITY_WINDOW : contains + WORKSPACE ||--o{ EVENT_TYPE : owns + USER ||--o{ EVENT_TYPE : hosts + AVAILABILITY_SCHEDULE ||--o{ EVENT_TYPE : supplies + + AVAILABILITY_SCHEDULE { + string id PK + string workspaceId FK + string userId FK + string name + string timeZone + boolean isDefault + datetime archivedAt + int version + } + + AVAILABILITY_WINDOW { + string id PK + string scheduleId FK + enum dayOfWeek + time localStart + time localEnd + int sortOrder + } + + EVENT_TYPE { + string id PK + string workspaceId FK + string hostUserId FK + string availabilityScheduleId FK + string title + string slug + int durationMinutes + int bufferBeforeMinutes + int bufferAfterMinutes + enum status + int version + } +``` + +### Availability invariants + +- Schedule timezone is a valid IANA timezone. +- Each window has `localStart < localEnd` under the initial same-day-window policy. +- Windows inside one schedule/day do not overlap. +- Full schedule replacement is transactional. +- A default schedule is unique within the selected owner scope. +- Archived schedules cannot be newly assigned to event types. +- Existing event types require a valid replacement before an assigned schedule is archived, or retain an accepted snapshot according to policy. + +### Event-type invariants + +- Slug is unique within workspace or public scheduling scope. +- Host is an active workspace member. +- Assigned schedule belongs to the same workspace and intended host. +- Duration is 5–480 minutes. +- Buffers are non-negative and bounded by an accepted maximum. +- Archived event types reject new direct bookings. +- Hard deletion is restricted after the event type has produced booking history. + +### Time-of-day representation + +`localStart` and `localEnd` are logical local wall-clock values. The final Prisma schema must choose among PostgreSQL `time`, integer minute-of-day, or another validated representation after testing: + +- Prisma type support +- Sorting and comparison +- Migration from `HH:mm` strings +- DST candidate generation behavior +- Cross-midnight requirements + +Initial recommendation: disallow cross-midnight windows and represent them as two weekday windows when needed. + +## Polling ER model + +```mermaid +erDiagram + WORKSPACE ||--o{ POLL : owns + USER ||--o{ POLL : creates + POLL ||--|{ POLL_CANDIDATE : proposes + POLL ||--o{ POLL_PARTICIPANT : includes + USER o|--o{ POLL_PARTICIPANT : optionally_identifies + POLL_PARTICIPANT ||--o{ POLL_PREFERENCE : expresses + POLL_CANDIDATE ||--o{ POLL_PREFERENCE : receives + POLL ||--o| POLL_FINALIZATION : concludes_with + POLL_CANDIDATE ||--o| POLL_FINALIZATION : selected_by + MEETING ||--o| POLL_FINALIZATION : produced_by + + POLL { + string id PK + string workspaceId FK + string createdByUserId FK + string title + string description + string slug UK + enum status + enum accessPolicy + enum resultVisibility + int durationMinutes + string timeZone + datetime responseDeadline + int version + } + + POLL_CANDIDATE { + string id PK + string pollId FK + datetime startAt + datetime endAt + int sortOrder + } + + POLL_PARTICIPANT { + string id PK + string pollId FK + string userId FK + string displayName + string normalizedEmail + enum role + enum responseStatus + string invitationTokenHash UK + string editTokenHash UK + datetime respondedAt + } + + POLL_PREFERENCE { + string id PK + string pollId FK + string participantId FK + string candidateId FK + enum status + datetime updatedAt + } + + POLL_FINALIZATION { + string pollId PK,FK + string candidateId FK + string meetingId FK,UK + string finalizedByUserId FK + datetime finalizedAt + json recommendationSnapshot + string overrideReason + } +``` + +### Poll lifecycle + +```mermaid +stateDiagram-v2 + [*] --> DRAFT + DRAFT --> OPEN: publish + DRAFT --> CANCELLED: discard + OPEN --> FINALIZED: finalize candidate + OPEN --> EXPIRED: deadline/policy + OPEN --> CANCELLED: cancel + EXPIRED --> OPEN: authorized extension + FINALIZED --> [*] + CANCELLED --> [*] +``` + +Initial recommendation: a finalized poll does not reopen. Rescheduling creates a new poll related to the meeting. + +### Participant semantics + +- A `PollParticipant` is scoped to one poll. +- `userId` is optional; accountless participation remains supported. +- Display name is presentation data, not identity. +- Email is stored once per participant, not once per candidate preference. +- Required/optional role influences decision policy. +- Absence of `PollPreference` represents unanswered; it is distinct from `NO`. +- Invitation and edit tokens are stored as hashes and never returned after initial issuance except through the original link-generation response. + +### Cross-poll integrity + +`PollPreference` includes `pollId` deliberately so the database can enforce composite consistency: + +```text +(participantId, pollId) -> PollParticipant(id, pollId) +(candidateId, pollId) -> PollCandidate(id, pollId) +``` + +This requires unique candidate keys on `(id, pollId)` for participant and candidate records. The exact Prisma relation syntax must be validated, but the relational invariant is accepted: + +> Preference participant and candidate belong to the same poll. + +### Finalization integrity + +`PollFinalization` is a one-to-one outcome record: + +- `pollId` is its primary key, so one poll finalizes once. +- `meetingId` is unique, so one meeting is not the outcome of several poll finalizations. +- Composite candidate/poll relationship proves selected candidate belongs to poll. +- Poll status transition, finalization insert, meeting creation, participants, meeting event, and outbox/notification intent occur in one transaction. + +### Poll constraints + +| Constraint | Purpose | +| --- | --- | +| Unique `slug` | Public poll identity under current route plan | +| Unique `(pollId, startAt, endAt)` | Prevent duplicate candidates | +| Check `endAt > startAt` | Valid candidate interval | +| Unique `(pollId, normalizedEmail)` when identity policy requires | Prevent duplicate invited email identity; conditional policy required | +| Unique `(participantId, candidateId)` | One effective preference per participant/candidate | +| Composite FKs including `pollId` | Prevent cross-poll preference/finalization references | + +## Meeting ER model + +```mermaid +erDiagram + WORKSPACE ||--o{ MEETING : owns + MEETING ||--|{ MEETING_PARTICIPANT : includes + USER o|--o{ MEETING_PARTICIPANT : optionally_identifies + MEETING ||--o{ MEETING_EVENT : records + USER o|--o{ MEETING_EVENT : optionally_acts + EVENT_TYPE o|--o{ DIRECT_BOOKING : sourced + MEETING ||--o| DIRECT_BOOKING : produced_from + + MEETING { + string id PK + string workspaceId FK + enum origin + enum status + string titleSnapshot + string descriptionSnapshot + datetime startAt + datetime endAt + string displayTimeZone + enum locationType + string locationValue + int version + datetime cancelledAt + datetime completedAt + } + + DIRECT_BOOKING { + string id PK + string workspaceId FK + string meetingId FK,UK + string eventTypeId FK + string guestNameSnapshot + string guestEmailSnapshot + string guestTimeZone + string guestNotes + string idempotencyKey + string managementTokenHash UK + datetime bookedAt + } + + MEETING_PARTICIPANT { + string id PK + string meetingId FK + string userId FK + string displayNameSnapshot + string normalizedEmailSnapshot + enum role + enum attendanceStatus + } + + MEETING_EVENT { + string id PK + string meetingId FK + string actorUserId FK + enum type + datetime occurredAt + int meetingVersion + json context + } +``` + +### Meeting origin + +Initial origin values: + +```text +DIRECT_BOOKING +POLL_FINALIZATION +MANUAL +``` + +Only add origin values backed by an implemented use case. + +### Meeting lifecycle + +```mermaid +stateDiagram-v2 + [*] --> SCHEDULED + SCHEDULED --> COMPLETED: completion policy + SCHEDULED --> CANCELLED: authorized cancellation + SCHEDULED --> SCHEDULED: reschedule with version increment + COMPLETED --> [*] + CANCELLED --> [*] +``` + +Rescheduling changes the interval, increments version, and appends a `RESCHEDULED` event. It does not require a misleading permanent `RESCHEDULED` current status. + +### Meeting invariants + +- `endAt > startAt`. +- Workspace owns the meeting. +- Meeting has at least one `HOST` participant. +- Participant user, when present, has valid relationship according to access policy. +- One direct booking produces at most one meeting. +- One poll finalization produces at most one meeting. +- Meeting origin agrees with exactly one accepted source relationship where applicable. +- Cancellation and completion timestamps agree with current status. +- Version increases on lifecycle/schedule mutation. +- External delivery/synchronization failure never rewrites meeting status. + +### Participant roles + +Initial roles: + +```text +HOST +REQUIRED +OPTIONAL +GUEST +``` + +Whether `GUEST` is meaningfully distinct from required/optional attendance should be reviewed during implementation. Avoid storing two overlapping classifications if one role dimension is insufficient; separate hosting role from attendance requirement if use cases demand it. + +### Direct booking snapshots + +`DirectBooking` retains accepted guest input and event provenance. `Meeting` retains title, description, and schedule snapshots required for independent historical meaning. + +`eventTypeId` should use restrictive or nullable retention behavior rather than cascading meeting deletion. If an event type is exceptionally hard-deleted through a privacy/maintenance process, the snapshot remains interpretable. + +### Meeting concurrency + +`version` supports optimistic concurrency: + +```text +UPDATE Meeting +SET ..., version = version + 1 +WHERE id = ? AND version = expectedVersion +``` + +Zero updated rows means the actor used stale state and must reload. + +### Overlap invariant + +The logical invariant is: + +> Blocking meeting intervals for the same host/resource do not overlap. + +Because hosts are represented through `MeetingParticipant`, the physical database constraint may require a dedicated reservation/host-allocation table or another representation suited to PostgreSQL exclusion constraints. This is intentionally unresolved in the logical ER model and requires a focused prototype before final Prisma design. + +## Notification ER model + +```mermaid +erDiagram + WORKSPACE ||--o{ NOTIFICATION : owns + MEETING o|--o{ NOTIFICATION : concerns + POLL o|--o{ NOTIFICATION : concerns + NOTIFICATION ||--o{ NOTIFICATION_ATTEMPT : attempts + + NOTIFICATION { + string id PK + string workspaceId FK + string meetingId FK + string pollId FK + enum purpose + enum channel + string templateKey + string recipientAddress + string recipientName + enum status + string deduplicationKey UK + datetime scheduledAt + datetime sentAt + datetime failedAt + } + + NOTIFICATION_ATTEMPT { + string id PK + string notificationId FK + int attemptNumber + string provider + string providerMessageId + enum outcome + string safeErrorCode + string safeErrorMessage + datetime attemptedAt + } +``` + +### Notification lifecycle + +```mermaid +stateDiagram-v2 + [*] --> PENDING + PENDING --> PROCESSING: claimed + PROCESSING --> SENT: provider accepted + PROCESSING --> RETRY_PENDING: retryable failure + PROCESSING --> FAILED: terminal failure + RETRY_PENDING --> PROCESSING: retry due + PENDING --> CANCELLED: business intent withdrawn + RETRY_PENDING --> CANCELLED: business intent withdrawn + SENT --> [*] + FAILED --> [*] + CANCELLED --> [*] +``` + +### Notification invariants + +- Notification represents one recipient, channel, purpose, and business occurrence. +- `deduplicationKey` prevents duplicate intent for the same logical occurrence. +- Attempt number is unique per notification. +- Provider message ID is retained when returned. +- Failure details are sanitized and never contain credentials or secret links. +- A notification references a valid subject context. The exact meeting/poll XOR constraint must be represented with a database check or a more general subject design. +- Required notification records are created in the same transaction as the business transition that requires them. + +### Outbox decision still required + +Two implementation options remain: + +1. `Notification` doubles as the durable outbox/work item. +2. A generic `OutboxMessage` is created transactionally and later produces notifications/integration work. + +System architecture will select the approach based on deployment, worker, replay, and non-email event requirements. + +## Calendar-integration ER model + +```mermaid +erDiagram + WORKSPACE ||--o{ CALENDAR_CONNECTION : owns + USER ||--o{ CALENDAR_CONNECTION : authorizes + CALENDAR_CONNECTION ||--o{ EXTERNAL_CALENDAR_EVENT : contains + MEETING ||--o{ EXTERNAL_CALENDAR_EVENT : synchronizes + + CALENDAR_CONNECTION { + string id PK + string workspaceId FK + string userId FK + enum provider + string externalAccountId + string credentialReference + enum status + datetime expiresAt + datetime revokedAt + } + + EXTERNAL_CALENDAR_EVENT { + string id PK + string connectionId FK + string meetingId FK + string externalCalendarId + string externalEventId + string providerVersion + enum syncStatus + datetime lastSyncedAt + string safeLastErrorCode + } +``` + +### Integration invariants + +- Provider account identity is unique within the intended connection scope. +- Credentials are encrypted or referenced from an appropriate secret store; never exposed through ordinary model serialization. +- External event identity is unique per connection/calendar. +- One meeting may have several external references when several hosts/calendars are synchronized. +- Webhook or retry reconciliation uses provider IDs and versions rather than meeting title/time matching. +- Connection revocation does not delete the internal meeting. + +## Proposed ownership map + +| Entity | Tenant owner | Acting user references | +| --- | --- | --- | +| AvailabilitySchedule | Workspace | Schedule user/host | +| EventType | Workspace | Host and creator where needed | +| Poll | Workspace | Creator/finalizer | +| Meeting | Workspace | Participants/actors through related records | +| DirectBooking | Same workspace as meeting | Accountless guest snapshot | +| Notification | Workspace | Recipient snapshot; initiating event actor elsewhere | +| CalendarConnection | Workspace | Authorizing user | + +Every resource query must establish workspace scope even when a globally unique ID is used. Global uniqueness is not tenancy authorization. + +## Aggregate and transaction boundaries + +### Direct booking transaction + +```text +Validate resource, availability, authorization, and idempotency + -> reserve/prevent overlapping interval + -> create Meeting + -> create host and guest MeetingParticipants + -> create DirectBooking source + -> append MeetingEvent(SCHEDULED) + -> create required Notification intents + -> commit +``` + +External email/calendar delivery occurs after commit. + +### Poll response transaction + +```text +Validate poll access and OPEN state + -> resolve PollParticipant + -> verify all candidates belong to poll + -> replace/upsert effective PollPreferences + -> update participant response state/time + -> commit +``` + +### Poll finalization transaction + +```text +Validate actor, poll version, OPEN state, and candidate + -> create Meeting and MeetingParticipants + -> create PollFinalization + -> update Poll to FINALIZED + -> append MeetingEvent(SCHEDULED) + -> create Notification/integration work + -> commit +``` + +### Meeting cancellation transaction + +```text +Validate actor, status, token/membership, and version + -> update Meeting to CANCELLED and increment version + -> append MeetingEvent(CANCELLED) + -> create notification/calendar-cancellation work + -> release internal reservation according to model + -> commit +``` + +### Meeting rescheduling transaction + +```text +Validate actor, availability, status, and expected version + -> replace interval and increment version + -> append MeetingEvent(RESCHEDULED) with previous/new interval + -> create notification/calendar-update work + -> update/finalize rescheduling poll when applicable + -> commit +``` + +## Proposed referential actions + +Referential actions should preserve historical commitments by default. + +| Relationship | Proposed behavior | Rationale | +| --- | --- | --- | +| User -> Account/session | Cascade | Authentication infrastructure depends on user | +| User -> Membership | Restrict or controlled removal | Workspace ownership transfer may be required | +| User -> participant/actor references | Set null where snapshot exists | Preserve history after account deletion | +| Workspace -> product resources | Restrict routine hard delete; archive first | Avoid accidental tenant-history loss | +| EventType -> DirectBooking | Restrict or SetNull with snapshot | Preserve booking provenance/history | +| Poll -> candidates/participants/preferences | Cascade only during controlled draft deletion; archive otherwise | Published decision history may matter | +| Meeting -> participants/events | Cascade only during controlled hard deletion | These are meeting-owned historical records | +| Meeting -> notification/external references | Preserve or controlled cascade according to retention | Operational history and reconciliation | + +Some policies cannot be expressed solely with `onDelete`; deletion should be a use case executed through domain services. + +## Access patterns and candidate indexes + +Indexes follow expected queries, not entity aesthetics. + +| Query | Candidate index | +| --- | --- | +| Find active membership | `WorkspaceMembership(userId, status)` and unique `(workspaceId, userId)` | +| List workspace event types | `EventType(workspaceId, status, createdAt)` | +| Load host schedules | `AvailabilitySchedule(workspaceId, userId, archivedAt)` | +| Load windows by schedule/day | `AvailabilityWindow(scheduleId, dayOfWeek, localStart)` | +| Resolve public event type | Unique `(workspaceId or publicOwnerScope, slug)` based on route decision | +| Resolve public poll | Unique `Poll(slug)` | +| List workspace polls | `Poll(workspaceId, status, createdAt)` | +| Load candidates | `PollCandidate(pollId, startAt)` | +| Track response progress | `PollParticipant(pollId, responseStatus)` | +| Load poll preferences | Unique `(participantId, candidateId)` plus candidate/poll consistency indexes | +| List upcoming meetings | `Meeting(workspaceId, status, startAt)` | +| Find user's hosted/attending meetings | `MeetingParticipant(userId, role, meetingId)` plus meeting join | +| Claim notifications | `Notification(status, scheduledAt)` | +| Load notification attempts | Unique `(notificationId, attemptNumber)` | +| Reconcile provider event | Unique `(connectionId, externalCalendarId, externalEventId)` | + +Query plans and data volume should be measured before final index selection. Foreign keys commonly queried for joins need explicit indexes in PostgreSQL unless already covered by a useful unique/composite index. + +## Current-to-proposed mapping + +| Current model/field | Proposed destination | Migration note | +| --- | --- | --- | +| `User` | `User` | Retain IDs; add onboarding/profile fields as needed | +| User-owned resources | Workspace-owned resources | Create personal workspace and owner membership per user | +| `User.timeZone` | `User.profileTimeZone` | Preserve as display/default preference | +| `UserAvailability` | `AvailabilitySchedule` + `AvailabilityWindow` | Parse JSON slots; create schedule timezone snapshot from current user timezone | +| `EventType.userId` | `EventType.workspaceId` + `hostUserId` | Map to personal workspace; preserve host | +| `Booking` | `Meeting` + `DirectBooking` + participants + initial event | Backfill one meeting per booking; preserve legacy ID mapping | +| `Booking.status` | `Meeting.status` | Map accepted/pending/cancelled through explicit migration policy | +| `googleMeetLink` / general meeting URL branch fields | Meeting location or external integration reference | Classify URL/provider; retain unknown as custom URL | +| `icsUid` from email feature branch | Stable calendar identity/external reference | Preserve existing UID during migration | +| `Poll` | `Poll` | Add workspace, lifecycle, access, duration, timezone, deadline, version | +| `TimeSlot` | `PollCandidate` | Preserve IDs where practical; validate intervals/duplicates | +| Repeated `Availability` vote rows | `PollParticipant` + `PollPreference` | Group by poll and participant identity heuristic; detect collisions | +| `Availability.userId` | `PollParticipant.userId` | Preserve optional account link | + +## Migration risks + +### Participant grouping + +Current free-form names are not reliable identities. Backfill cannot safely assume that every case-insensitive matching name is one person. + +Migration should: + +- Group by poll +- Prefer existing `userId` when present +- Use normalized email when consistently present +- Detect conflicting emails/names +- Produce a review/report for ambiguous groups +- Avoid silently merging distinct people + +### Schedule JSON quality + +Existing JSON may contain fallback windows, invalid ordering, or unexpected shapes. Migration must validate and report before enforcing new constraints. + +### Booking overlap + +Existing data must be scanned for overlaps before adding an exclusion/reservation constraint. Invalid historical data needs an explicit resolution policy. + +### Cascade changes + +Changing from cascade to restrict can fail while orphaning assumptions exist in application flows. Delete actions must be changed before the constraint cutover. + +### Feature-branch divergence + +Email-invitation schema changes such as stable ICS identity and general meeting provider fields must be merged or reconciled before writing the final migration. Do not design migration from an outdated branch snapshot. + +## Incremental migration phases + +### Phase 0: Reconcile and inspect + +- Merge/reconcile active schema feature branches. +- Back up development/staging data. +- Run data-quality reports for overlaps, invalid windows, duplicate poll identities, and cross-poll votes. +- Freeze final naming and enum mappings for the migration. + +### Phase 1: Add workspace ownership + +- Add workspace, membership, and invitation tables. +- Create one personal workspace per existing user. +- Backfill workspace IDs onto current resource tables. +- Update authorization/query paths. +- Add non-null constraints only after verification. + +### Phase 2: Normalize schedules + +- Add schedule/window tables. +- Convert current weekly JSON windows. +- Compare generated candidates between old and new engines. +- Switch reads/writes after parity tests. +- Retire duplicated legacy schedule columns later. + +### Phase 3: Introduce meetings for current bookings + +- Add meeting, participant, direct-booking source, and event tables. +- Backfill one meeting per booking. +- Preserve booking status, time, guest, event-type snapshot, meeting URL, and calendar identity. +- Temporarily retain legacy booking lookup mapping. +- Switch booking creation to the new transaction. + +### Phase 4: Normalize poll participation + +- Add poll lifecycle and participant/preference tables. +- Convert candidates and group current votes conservatively. +- Add composite cross-poll constraints. +- Switch voting to participant identity and explicit unanswered behavior. + +### Phase 5: Add poll finalization + +- Add finalization relationship and state transition. +- Implement poll-to-meeting transaction. +- Add idempotency and concurrency tests. +- Add participant notifications. + +### Phase 6: Introduce durable work and integrations + +- Add notification/attempt model or generic outbox after system-architecture decision. +- Migrate booking email state where available. +- Add calendar connections and external event references with one provider first. + +### Phase 7: Remove legacy structures + +- Confirm no active code reads legacy models/columns. +- Run reconciliation reports. +- Remove or rename legacy tables through explicit migrations. +- Update current-domain documentation to match the new implemented model. + +## Verification strategy + +### Schema verification + +- Migration applies to empty and representative populated databases. +- Every backfill reports counts before and after. +- Foreign keys, unique constraints, checks, and indexes exist as designed. +- Rollback or forward-fix procedure is documented per phase. + +### Domain verification + +- Direct booking creates one meeting and source. +- Concurrent booking attempts cannot violate overlap rules. +- Poll preference cannot reference another poll's candidate. +- Repeated finalization creates one meeting. +- Cancellation/rescheduling preserve lifecycle history. +- User deletion/anonymization follows retention policy. +- Provider failure leaves internal meeting state intact. + +### Journey verification + +End-to-end tests should cover: + +- First-user personal workspace provisioning +- Direct booking success and notification failure +- Open-link and invitation-only poll response +- Poll finalization +- Direct and consensus rescheduling +- Cancellation with provider retry state + +## Deferred decisions + +- Exact PostgreSQL overlap mechanism +- Exact local-time physical type +- Generic outbox versus notification-as-work-item +- Meeting participant role dimensionality +- Workspace-specific custom domains +- Multiple availability schedules in initial UI +- Multiple calendar providers +- Formal retention durations + +Deferred does not mean forgotten; each item has a known decision point before implementation. + +## Acceptance checklist + +- [ ] Direct booking and poll finalization converge on `Meeting`. +- [ ] `DirectBooking` preserves source-specific guest/provenance data. +- [ ] Poll participant identity no longer depends on name. +- [ ] Composite relationships prevent cross-poll preferences/finalization. +- [ ] Workspace ownership and user actor roles are separate. +- [ ] Schedule timezone is independent of profile timezone. +- [ ] Current status and lifecycle history serve different queries. +- [ ] Configuration deletion cannot casually erase meeting history. +- [ ] Notification and provider state are independent from meeting truth. +- [ ] Every current table has a migration destination. +- [ ] Unresolved physical mechanisms are labelled rather than guessed. + +## Related runtime and delivery design + +The [system architecture](./system-architecture.md) defines how the modular monolith realizes this model. The [deployment architecture](./deployment-architecture.md) maps it to runtime infrastructure, and the [roadmap](./roadmap.md) defines incremental migration and implementation gates. diff --git a/docs/feature-inventory.md b/docs/feature-inventory.md index dc6fba3..2f032ad 100644 --- a/docs/feature-inventory.md +++ b/docs/feature-inventory.md @@ -1,7 +1,7 @@ # Feature Inventory **Document status:** Initial draft -**Last reviewed against repository:** 2026-08-25 +**Last reviewed against repository:** 2026-08-29 This inventory separates implemented behavior from proposals. It is not a marketing feature list. @@ -21,7 +21,7 @@ This inventory separates implemented behavior from proposals. It is not a market | GitHub OAuth authentication | Current | Implemented with Auth.js. First OAuth login also acts as account creation. | | Protected dashboard pages | Current | Availability, event types, and bookings verify the authenticated session. | | New-user onboarding | Proposed | Username, timezone, initial availability, and first scheduling action. | -| Google OAuth login | Proposed | Keep identity scopes separate from Google Calendar permissions. | +| Google OAuth authentication | Current | Implemented with Auth.js; identity scopes must remain separate from future Google Calendar permissions. | | Email magic-link login | Exploratory | A possible alternative for users without supported OAuth providers. | | Password authentication | Out of scope | Adds password storage, reset, verification, and abuse-prevention responsibilities without current product value. | | Personal workspace | Proposed | Created automatically for each user without exposing unnecessary organization UI. | @@ -48,10 +48,10 @@ This inventory separates implemented behavior from proposals. It is not a market | Capability | Status | Notes | | --- | --- | --- | -| Guest confirmation email | Current | Rendered with React Email and sent with Resend. | -| ICS attachment | Current | Generated with a stable UID stored on the booking. | -| Guest-timezone email formatting | Current | Confirmation date and time use the guest timezone. | -| Separate booking and email outcomes | Current | Email failure is reported without changing successful booking state. | +| Guest confirmation email | Partial | Implemented with React Email and Resend on the email-invitation feature branch; pending baseline reconciliation. | +| ICS attachment | Partial | Stable booking UID and attachment generation exist on the email-invitation feature branch; pending baseline reconciliation. | +| Guest-timezone email formatting | Partial | Implemented on the email-invitation feature branch; pending baseline reconciliation. | +| Separate booking and email outcomes | Partial | Implemented on the email-invitation feature branch; durable delivery state remains proposed. | | Persisted notification status | Proposed | Needed for delivery history, retries, and operational visibility. | | Retry failed notification | Proposed | Must include authorization, idempotency, and abuse controls. | | Host notification | Proposed | Host should receive or configure booking notifications. | diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..cfd1f04 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,389 @@ +# Product and Architecture Roadmap + +**Document status:** Proposed delivery plan + +**Scope:** Vertical milestones from the current application to the proposed product architecture + +**Related documents:** [Feature inventory](./feature-inventory.md), [system architecture](./system-architecture.md), [deployment architecture](./deployment-architecture.md) + +## Purpose + +This roadmap turns the product, domain, and architecture documents into an implementation sequence. It is organized around user outcomes rather than technical layers: each milestone should leave the application more coherent and demonstrable. + +Dates and commercial commitments are intentionally excluded. Priority depends on validated product learning and completion evidence, not an artificial calendar. + +## Delivery principles + +1. Build one complete vertical slice before broad infrastructure extraction. +2. Preserve existing behavior with characterization tests before changing its model. +3. Treat migrations and backfills as product work, not release afterthoughts. +4. Keep current, partial, and proposed capabilities clearly labelled. +5. Commit internal state before unreliable provider work. +6. Enforce concurrency-sensitive invariants in PostgreSQL. +7. Improve the connected user journey alongside domain changes. +8. Extract packages and services only when evidence supports the boundary. + +## Roadmap overview + +```mermaid +flowchart LR + M0[M0 Reconcile and baseline] + M1[M1 Connected product shell] + M2[M2 Poll to meeting] + M3[M3 Direct booking hardening] + M4[M4 Meeting lifecycle] + M5[M5 Durable notifications] + M6[M6 Calendar integration] + M7[M7 Fair scheduling] + M8[M8 Collaboration] + M9[M9 Production validation] + + M0 --> M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7 --> M8 --> M9 +``` + +Some work can overlap after its dependencies are stable, but the arrows identify the safest default sequence. + +## M0: Reconcile and establish the baseline + +### User outcome + +The existing booking, poll, authentication, and email behavior has one trustworthy integrated baseline. + +### Scope + +- Merge or deliberately reconcile the email-invitation work with the target branch. +- Verify Google and GitHub authentication behavior and environment contracts. +- Run the current test/build/lint suite. +- Compare Prisma schema and generated migrations with the current-domain document. +- Classify existing features as current, partial, or proposed from one commit. +- Record known failures and data-quality queries before schema migration. + +### Architecture work + +- Preserve characterization tests around direct booking and polling. +- Establish stable error-result conventions for Server Actions. +- Add a basic correlation/deploy identifier to server-side diagnostics when deployment work starts. +- Do not reorganize all folders in this milestone. + +### Completion evidence + +- One branch contains the intended baseline. +- Current documentation matches that commit. +- Booking success and email failure remain separate outcomes. +- Tests and production builds pass from the monorepo root or have documented gaps. + +## M1: Create a connected product shell + +### User outcome + +A new or returning host understands what to do next and can move naturally between direct scheduling and polls. + +### Scope + +- Add a real dashboard overview. +- Unify navigation for event types, availability, meetings/bookings, and polls. +- Preserve locale across links, redirects, and actions. +- Add consistent loading, empty, success, and error states. +- Add a scheduling-readiness checklist. +- Provide clear poll vote confirmation. +- Improve host-timezone display on management pages. + +### Architecture work + +- Keep route components focused on composition and reads. +- Introduce shared UI patterns only where repeated behavior exists. +- Define accessibility expectations for keyboard, focus, status announcements, and contrast. + +### Completion evidence + +- A first-time evaluator can create and share either scheduling resource without guessing the navigation. +- Critical navigation is locale-safe. +- Automated accessibility checks cover primary forms, with a documented manual keyboard pass. + +## M2: Deliver the poll-to-meeting vertical slice + +### User outcome + +An organizer can create a practical multi-date poll, collect identifiable responses, select a candidate, and produce a confirmed meeting. + +### Scope + +- Add explicit poll lifecycle and access policy. +- Support multiple dates, duration, organizer timezone, and candidate intervals. +- Introduce poll-scoped participants and preferences. +- Support registered and accountless participants securely. +- Distinguish required and optional participants. +- Add poll management and response progress. +- Finalize one candidate into one shared `Meeting`. +- Lock further responses after finalization. + +### Domain and data work + +- Introduce `PollCandidate`, `PollParticipant`, and `PollPreference` relationships. +- Prevent cross-poll participant/candidate references structurally. +- Introduce `Meeting`, `MeetingParticipant`, `MeetingEvent`, and `PollFinalization`. +- Make repeated/concurrent finalization produce one outcome. +- Create personal workspaces as the invisible ownership boundary if required by the selected migration slice. + +### Modular-monolith work + +Use this as the first intentionally modular slice: + +```text +Polling.finalizePoll() + -> Meetings.createFromPoll() + -> record follow-up work +``` + +Server Actions remain delivery adapters; application services own authorization and the transaction. + +### Completion evidence + +- A tampered response cannot vote on another poll's candidate. +- A double-click or concurrent finalization creates one meeting. +- The meeting contains the selected interval and participant snapshots. +- End-to-end coverage proves create, respond, finalize, and view-confirmation journeys. + +## M3: Harden direct booking onto the meeting model + +### User outcome + +A guest can reserve a valid time exactly once, and the host sees the same meeting model produced by polls. + +### Scope + +- Make submitted-slot validation authoritative at submission time. +- Apply event-type duration and buffers consistently. +- Reject past slots and enforce notice-window rules. +- Validate IANA timezones. +- Add request idempotency. +- Preserve booking provenance through `DirectBooking`. +- Create the shared `Meeting`, participants, and lifecycle event. + +### Database decision gate + +Prototype and select the PostgreSQL host-overlap mechanism before migration. Record the selected approach in an ADR because it is concurrency-sensitive and expensive to reverse. + +### Completion evidence + +- Concurrent overlapping attempts cannot both succeed. +- Retrying the same logical submission returns one booking outcome. +- Direct booking and poll finalization produce the same meeting lifecycle shape. +- DST and timezone-boundary tests cover representative transitions. + +## M4: Complete the meeting lifecycle + +### User outcome + +Hosts and authorized participants can understand, cancel, and reschedule meetings without contradictory internal or calendar state. + +### Scope + +- Replace the incomplete bookings list with meeting list and detail views. +- Add upcoming, past, and cancelled filters. +- Add authorized cancellation with actor, timestamp, and reason. +- Add direct rescheduling while preserving meeting identity and history. +- Add consensus rescheduling through a related poll. +- Add optimistic concurrency for meeting changes. +- Archive configuration instead of cascading away history. + +### Completion evidence + +- Cancellation stops blocking availability according to policy. +- Concurrent edits do not silently overwrite newer state. +- Rescheduling preserves previous schedule history and stable calendar identity. +- Abandoning a rescheduling poll leaves the current meeting unchanged. + +## M5: Introduce durable notifications + +### User outcome + +Invitations, updates, cancellations, and retries are reliable and their delivery state is visible without changing meeting truth. + +### Scope + +- Add `Notification` and `NotificationAttempt` persistence. +- Record notification intent in the business transaction. +- Select and implement the Vercel-compatible worker trigger. +- Add idempotent provider delivery and bounded retry. +- Support host and participant notifications. +- Preserve ICS UID and correct sequence/status for lifecycle changes. +- Add authorized manual retry for terminal failures. +- Add delivery monitoring and safe failure details. + +### Decision gate + +Evaluate notification-as-work-item with Vercel Cron, Vercel Queues, and other appropriate mechanisms. Record an ADR if choosing a generic outbox or a deeper platform commitment. + +### Completion evidence + +- Provider outage does not roll back or duplicate a meeting. +- A crashed worker can safely retry claimed work. +- Repeated processing does not send a new logical notification unintentionally. +- Operational views expose pending age and terminal failures. + +## M6: Add external calendar and conferencing integration + +### User outcome + +Hosts can avoid external calendar conflicts and synchronize confirmed meetings without losing SlotSyncro's internal authority. + +### Scope + +- Add calendar authorization separately from login consent. +- Read free/busy data with minimal scopes. +- Include external conflicts in candidate generation. +- Create, update, and cancel external calendar events idempotently. +- Add conferencing-link provider support through an adapter. +- Verify, deduplicate, and reconcile provider webhooks. +- Encrypt persisted provider refresh tokens. + +### Completion evidence + +- Revoked provider access degrades safely. +- A missed or duplicated webhook does not corrupt meeting state. +- Reconciliation detects and repairs recoverable drift. +- Provider-specific fields stay outside the core meeting model. + +## M7: Deliver explainable fair scheduling + +### User outcome + +Groups receive recommendations that account for preferences, required attendance, and timezone inconvenience—and can understand the ranking. + +### Scope + +- Define and test a timezone inconvenience model. +- Add participant working-hour preferences. +- Generate smarter candidates from duration, availability, calendars, and buffers. +- Rank candidates using explicit required/optional participant policy. +- Explain recommendation factors in the UI. +- Evaluate rotating burden for recurring cross-timezone groups. +- Measure recommendation usefulness rather than claiming fairness from a score alone. + +### Completion evidence + +- Identical inputs produce deterministic rankings. +- Explanations match the actual scoring inputs. +- DST and unusual timezone offsets are tested. +- User research or structured feedback evaluates whether recommendations feel useful and fair. + +## M8: Add collaboration when ownership demands it + +### User outcome + +Multiple authenticated people can manage shared scheduling resources with clear roles and history. + +### Scope + +- Expose team workspace creation only after shared ownership use cases are validated. +- Add memberships, invitations, role changes, expiration, and revocation. +- Move selected event types, schedules, polls, and meetings to workspace ownership. +- Add audit history for administrative changes. +- Define deletion, departure, and ownership-transfer policies. + +Personal workspaces should exist earlier as an invisible tenancy boundary; this milestone is about exposing collaboration UX, not retrofitting ownership from scratch. + +### Completion evidence + +- Every workspace mutation enforces role policy in the application service. +- Removing a member has a defined effect on owned and assigned resources. +- Invitation tokens are hashed, expiring, purpose-limited, and revocable. +- Administrative changes have sufficient audit history. + +## M9: Validate production operation + +### User outcome + +The product remains trustworthy through releases and recoverable failures. + +### Scope + +- Configure separate Vercel projects for marketing and product. +- Isolate preview and production databases and provider credentials. +- Execute reviewed, serialized production migrations. +- Add deployment smoke tests for critical journeys. +- Define service indicators and actionable alerts. +- Enable managed backups and point-in-time recovery. +- Set initial RTO and RPO. +- Run and document a restore/reconciliation exercise. +- Review privacy, retention, rate limiting, and public-endpoint abuse controls. + +Deployment begins before this milestone; this milestone proves the operational guarantees rather than merely obtaining a public URL. + +### Completion evidence + +- A preview cannot mutate production data. +- A production migration has a documented verification and recovery path. +- A restore exercise meets or informs the stated recovery targets. +- Failed notification/calendar work is observable and recoverable. +- Critical user journeys are verified after deployment. + +## Cross-cutting test strategy + +Each milestone selects the lowest-cost test that provides credible evidence: + +| Risk | Required evidence | +| --- | --- | +| Pure business calculation | Unit tests | +| Authorization and orchestration | Application-service tests | +| Constraint, migration, or transaction | Real PostgreSQL integration tests | +| Provider mapping/signature | Adapter contract tests | +| Critical user outcome | End-to-end tests | +| Concurrency | Purpose-built parallel integration test | +| Accessibility | Automated checks plus manual keyboard/screen-reader review where relevant | +| Recovery | Operational exercise and reconciliation report | + +Coverage percentage is supporting information, not proof that important risks are tested. + +## Documentation and decision gates + +Before each milestone implementation: + +1. Confirm the user journey and use-case scope. +2. Resolve business-rule questions that block correctness. +3. Update the logical/physical data design. +4. Record an ADR for long-lived, cross-cutting, expensive decisions. +5. Define migration and test evidence. + +After implementation: + +1. Update feature statuses and current-domain documentation. +2. Check that proposed statements are not presented as shipped behavior. +3. Record measured outcomes, limitations, and follow-up work. +4. Capture a content opportunity only when the lesson is reusable and evidence is safe to publish. + +## Recommended immediate backlog + +The first executable backlog after this documentation checkpoint is: + +1. Reconcile the email-invitation branch and main. +2. Run and document the baseline validation suite. +3. Fix dashboard/navigation and locale continuity. +4. Write the poll-to-meeting migration proposal against the reconciled schema. +5. Implement the poll-to-meeting application service and transaction. +6. Add database and end-to-end tests for cross-poll integrity and repeated finalization. + +## Explicitly deferred + +- Billing and monetization +- Public API and general outgoing webhooks +- Embeddable scheduler +- Custom domains +- Multiple external calendar providers +- Automatic poll finalization +- Quorum-heavy governance rules +- Independent microservices + +Deferred capabilities may be reconsidered after core scheduling, lifecycle, reliability, and product coherence are demonstrated. + +## Roadmap acceptance checklist + +- [ ] Milestones deliver user outcomes rather than isolated technical layers. +- [ ] Poll and direct-booking paths converge on the shared meeting lifecycle. +- [ ] Concurrency and migration decisions occur before risky schema implementation. +- [ ] UX coherence is improved before adding broad feature depth. +- [ ] External-provider reliability is separated from internal business truth. +- [ ] Workspace collaboration follows a stable ownership boundary. +- [ ] Fairness claims require explainable logic and user evidence. +- [ ] Production readiness includes recovery exercises, not only deployment. diff --git a/docs/system-architecture.md b/docs/system-architecture.md new file mode 100644 index 0000000..9f9fd66 --- /dev/null +++ b/docs/system-architecture.md @@ -0,0 +1,452 @@ +# Proposed System Architecture + +**Document status:** Proposed runtime design + +**Scope:** How the modular monolith executes SlotSyncro use cases + +**Related decisions:** [ADR 0001](./adr/0001-adopt-modular-monolith.md), [proposed domain model](./domain-model-proposed.md) + +## Purpose + +The domain model defines the information SlotSyncro must preserve. This document defines how requests, business rules, database transactions, and external providers cooperate to preserve it. + +This is a target architecture, not a claim that every boundary already exists in code. The current application has Server Actions that combine several responsibilities. Migration will happen one vertical slice at a time. + +## Architectural style + +`apps/app` is one deployable Next.js modular monolith backed by one PostgreSQL database. Modules are logical ownership boundaries inside the process, not separately deployed services. + +```mermaid +flowchart TB + Browser[Browser] + Delivery[Next.js pages, route handlers, Server Actions] + Modules[Application module APIs] + Domain[Domain policies and state transitions] + Persistence[Repositories and transaction coordinator] + Database[(PostgreSQL)] + Worker[Background worker or scheduled dispatcher] + Adapters[Provider adapters] + Providers[Email, calendar, conferencing providers] + + Browser --> Delivery + Delivery --> Modules + Modules --> Domain + Modules --> Persistence + Persistence --> Database + Worker --> Database + Worker --> Adapters + Adapters --> Providers + Providers -. webhook .-> Delivery +``` + +The boundaries have different responsibilities: + +| Boundary | Owns | Must not own | +| --- | --- | --- | +| Delivery | Transport parsing, authentication context, invoking a use case, mapping results | Core business rules or scattered Prisma mutations | +| Application | Use-case orchestration, authorization, transaction scope, idempotency | React rendering or provider SDK details | +| Domain | Invariants, policies, value objects, state transitions | Next.js, Prisma, Resend, or HTTP concepts | +| Persistence | Queries, writes, database constraint translation | Product policy decisions | +| Integration | Provider request/response mapping, signatures, external IDs | Authority over internal meeting truth | + +## Module map and ownership + +```mermaid +flowchart LR + Identity[Identity] + Workspace[Workspace] + Scheduling[Scheduling] + Availability[Availability] + Booking[Booking] + Polling[Polling] + Meetings[Meetings] + Notifications[Notifications] + Integrations[Integrations] + + Identity --> Workspace + Scheduling --> Workspace + Availability --> Scheduling + Booking --> Scheduling + Booking --> Availability + Booking --> Meetings + Polling --> Meetings + Meetings --> Workspace + Meetings --> Notifications + Meetings --> Integrations +``` + +The arrows show allowed use-case dependencies, not database foreign keys. Cycles are design feedback. If two modules need each other, move the shared decision to the module that owns the business outcome or coordinate them from an application-level use case. + +| Module | Public responsibilities | +| --- | --- | +| Identity | Resolve the authenticated actor and account identity | +| Workspace | Authorize workspace roles and manage membership | +| Scheduling | Manage event types and scheduling configuration | +| Availability | Generate bookable intervals and evaluate conflict inputs | +| Booking | Validate and accept a direct-booking request | +| Polling | Create polls, record preferences, recommend and finalize a candidate | +| Meetings | Create, cancel, reschedule, and read the scheduled commitment | +| Notifications | Record delivery intent and manage delivery lifecycle | +| Integrations | Synchronize calendar/conferencing state through provider adapters | + +Modules should expose use-case-oriented operations rather than their tables: + +```ts +// Illustrative contracts, not committed source code. +createDirectBooking(command, context) +recordPollPreference(command, context) +finalizePoll(command, context) +cancelMeeting(command, context) +``` + +An API such as `updateMeetingRow()` would expose persistence rather than business intent and make invariants easier to bypass. + +## Request and use-case flow + +For a state-changing request: + +```text +UI + -> Server Action or route handler + -> parse and validate transport input + -> establish actor and request context + -> call one application use case + -> authorize against the owned resource + -> execute domain rules inside a database transaction + -> return a typed result + -> update UI or revalidate affected reads +``` + +### Delivery adapters + +Server Actions are appropriate for application-owned browser mutations. Route handlers remain useful for: + +- Provider webhooks +- Public machine-facing endpoints +- OAuth callbacks handled by the authentication integration +- Background-job endpoints when the deployment platform requires them + +Delivery adapters should: + +- Validate untrusted input +- Construct an explicit actor/request context +- Call one application operation +- Translate expected failures into stable result codes +- Avoid exposing raw database or provider errors + +Client components should never import Prisma or provider SDKs. + +### Application services + +An application service coordinates a complete use case. For example, poll finalization must verify authorization and poll state, select a candidate, create the meeting outcome, preserve lifecycle history, and record durable follow-up work. + +Application services may call several module-owned policies, but the caller sees one atomic operation. + +### Domain policies + +Domain code represents rules such as: + +- A candidate must belong to the poll being finalized. +- A finalized poll cannot be finalized again. +- A meeting end must be later than its start. +- An unanswered poll candidate is not equivalent to a `NO` preference. +- External delivery failure does not cancel a confirmed meeting. + +Pure policies should be testable without Next.js or a database. Rules dependent on current persisted state are completed inside the transaction that changes that state. + +## Persistence and transaction management + +`packages/db` continues to own Prisma configuration and schema generation. Product modules own the meaning of their data and access it through focused repository/query functions. + +Repositories are useful when they: + +- Give a business operation a stable persistence interface +- Centralize a non-trivial query or locking strategy +- Prevent another module from mutating owned records directly +- Translate known database constraint failures into application errors + +They should not become generic wrappers around every Prisma method. + +### Transaction rule + +One business decision that must be all-or-nothing uses one local database transaction. Do not include slow network calls inside that transaction. + +```text +Inside transaction + -> authorize against current state + -> re-check mutable invariants + -> write aggregate state and lifecycle event + -> write notification/integration work + -> commit + +After commit + -> worker claims durable work + -> call external provider + -> record success or retryable/permanent failure +``` + +Database constraints remain the final protection for concurrency-sensitive invariants. Pre-checks improve error messages but do not replace unique, foreign-key, check, or overlap constraints. + +## Direct-booking sequence + +```mermaid +sequenceDiagram + actor Guest + participant Action as Booking Server Action + participant Booking as Booking application service + participant Availability as Availability policy + participant DB as PostgreSQL transaction + participant Worker as Notification worker + participant Email as Email provider + + Guest->>Action: Submit booking and idempotency key + Action->>Booking: createDirectBooking(command, guestContext) + Booking->>DB: Load event type and host configuration + Booking->>Availability: Validate interval and policy + Booking->>DB: Protect overlap and create Meeting + DirectBooking + Booking->>DB: Create participants, event, notification intent + DB-->>Booking: Commit + Booking-->>Action: Confirmed meeting result + Action-->>Guest: Show confirmation + Worker->>DB: Claim pending notification + Worker->>Email: Send invitation + Email-->>Worker: Provider result + Worker->>DB: Record attempt and final/retry state +``` + +The user receives a confirmed meeting after internal state commits. Email status is reported separately; a provider timeout must not create a second booking when the request is retried. + +## Poll-finalization sequence + +```mermaid +sequenceDiagram + actor Organizer + participant Action as Poll Server Action + participant Polling as Polling application service + participant DB as PostgreSQL transaction + participant Meetings as Meetings module + participant Worker as Background worker + + Organizer->>Action: Finalize selected candidate + Action->>Polling: finalizePoll(command, actorContext) + Polling->>DB: Load and lock current poll state + Polling->>DB: Verify actor, OPEN state, and candidate ownership + Polling->>Meetings: Build meeting outcome + Polling->>DB: Create Meeting, participants, event, and finalization + Polling->>DB: Mark poll FINALIZED and record durable work + DB-->>Polling: Commit + Polling-->>Organizer: Finalized meeting result + Worker->>DB: Claim notification/calendar work +``` + +Poll status, finalization, and meeting creation belong to the same transaction. Repeating the command must return the existing outcome or a stable already-finalized result, never create a second meeting. + +## Durable background work + +The initial recommended design is **notification-as-work-item**: + +- `Notification` stores the durable intent and lifecycle. +- `NotificationAttempt` stores each provider attempt. +- A worker atomically claims eligible notifications. +- Retry timing and attempt limits are explicit. +- A stable idempotency key prevents duplicate logical work. + +This is simpler than introducing a generic event outbox before multiple reliable consumers exist. Adopt a generic outbox later if calendar synchronization, analytics, webhooks, or other consumers need the same committed domain events independently. + +```mermaid +stateDiagram-v2 + [*] --> PENDING + PENDING --> PROCESSING: claimed + PROCESSING --> SENT: provider accepted + PROCESSING --> RETRY_PENDING: transient failure + RETRY_PENDING --> PROCESSING: retry due + PROCESSING --> FAILED: permanent/exhausted + PENDING --> CANCELLED: no longer applicable + RETRY_PENDING --> CANCELLED: no longer applicable +``` + +Worker claims must use a lease or equivalent atomic update so crashed workers do not leave work permanently stuck and concurrent workers do not deliver the same item intentionally. + +## Integrations and provider adapters + +Internal contracts must use SlotSyncro concepts. Adapters translate those contracts to Resend, Google Calendar, or future providers. + +```text +Notification use case -> EmailSender interface -> Resend adapter +Calendar sync use case -> CalendarProvider interface -> Google adapter +``` + +Provider IDs, payload fragments, sync tokens, and errors belong to integration-owned records. `Meeting` remains authoritative for SlotSyncro scheduling state. + +### Webhooks + +A provider webhook handler must: + +1. Preserve the raw request long enough to verify its signature. +2. Reject invalid or stale requests. +3. Deduplicate by provider event ID. +4. Resolve the relevant integration record. +5. Apply an allowed state transition through an application use case. +6. Return promptly; defer slow secondary work. + +A webhook is evidence from a provider, not permission to bypass workspace authorization or meeting lifecycle rules. + +## Authentication and authorization + +Authentication answers **who is acting**. Authorization answers **whether that actor may perform this operation on this resource**. + +The application uses explicit actor contexts: + +```text +AuthenticatedActor(userId, session metadata) +GuestActor(verified capability/token claims) +SystemActor(job or verified provider webhook) +``` + +### Workspace authorization + +Authenticated operations resolve the resource's workspace and require an active membership with sufficient role. A user ID on a session proves identity, not ownership of every supplied record ID. + +Authorization should occur in the application service using records loaded for the mutation. UI visibility checks improve experience but are not security controls. + +### Accountless access + +Public booking and poll participation must not gain broad access merely from knowing a slug or record ID. + +- Public slugs identify a resource; policy determines what is publicly readable or writable. +- Invitation-only access uses random, expiring, purpose-limited tokens. +- Store token hashes, not reusable plaintext tokens. +- Compare tokens safely and support revocation/rotation. +- An edit token grants only the stated participant operation, not workspace membership. + +## Idempotency and concurrency + +Idempotency means safely repeating the same logical command. It is required where browsers, workers, or providers may retry. + +Candidate operations include: + +- Direct booking creation +- Poll finalization +- Notification delivery +- Calendar event creation +- Webhook consumption + +Use a stable operation key plus a uniqueness constraint. The same key with incompatible payload data must be rejected rather than silently reused. + +Optimistic versioning can protect ordinary meeting edits. Booking overlap requires a database-backed mechanism designed in the physical schema; a read-then-create check alone is unsafe under concurrency. + +## Error model + +Expected failures should have stable application codes while logs retain technical detail. + +| Category | Example | User-facing behavior | +| --- | --- | --- | +| Validation | End before start | Explain the field problem | +| Authentication | Missing/expired session | Request sign-in or token renewal | +| Authorization | Non-member finalizes poll | Deny without revealing private data | +| Conflict | Slot taken or version stale | Refresh choices and retry intentionally | +| Not found | Unknown public slug | Neutral not-found response | +| Provider | Email temporarily unavailable | Preserve meeting and expose delivery state where useful | +| Unexpected | Database/runtime defect | Generic response plus traceable internal error | + +Do not return raw Prisma or provider errors to clients. + +## Observability + +Every request and background attempt should carry a correlation ID. Structured logs should include safe identifiers and transitions, not secrets or full personal payloads. + +Minimum useful signals: + +- Use-case success, expected failure category, and latency +- Transaction conflicts and database-constraint failures +- Notification queue depth, oldest pending age, attempts, and terminal failures +- Provider latency and error category +- Webhook signature failures and duplicate counts +- Meeting/poll lifecycle transitions with actor category + +Audit history such as `MeetingEvent` serves product and support questions. Operational logs and metrics serve system diagnosis. Neither replaces the other. + +## Security boundaries + +- Browser input, public URLs, provider callbacks, and job triggers are untrusted boundaries. +- Secrets stay in server runtime configuration and are never passed to client components. +- Personally identifiable information is minimized in logs and provider metadata. +- Provider OAuth tokens require encryption at rest and restricted access paths. +- Rate limits apply to public booking, poll participation, token verification, and webhook endpoints. +- Database access remains server-only. + +## Proposed physical organization + +```text +apps/app/ +├── app/ # Next.js delivery adapters +├── components/ # Shared and route-composed UI +└── modules/ + ├── booking/ + │ ├── actions/ # Optional module-local delivery adapters + │ ├── application/ # Use cases and ports + │ ├── domain/ # Rules/value objects when complexity warrants + │ ├── infrastructure/ # Prisma/provider implementations + │ └── tests/ + ├── polling/ + ├── meetings/ + ├── notifications/ + └── integrations/ +``` + +This is a direction, not a requirement to create every folder immediately. Begin with a public use-case function and colocated tests; add layers when they isolate real complexity. + +## Testing by boundary + +| Test type | Primary confidence | +| --- | --- | +| Pure unit | Domain rules, scoring, state transitions, value objects | +| Application service | Authorization, orchestration, stable errors, idempotent behavior | +| Database integration | Constraints, transactions, locking, indexes, repository mappings | +| Adapter contract | Provider translation, signatures, error classification | +| End-to-end | Critical user journeys across UI, server, and database | + +Mocks are useful at provider boundaries. Database invariants and concurrency behavior require a real PostgreSQL integration environment. + +## Incremental adoption + +1. Preserve current behavior and add characterization tests. +2. Implement poll finalization as the first explicit modular vertical slice. +3. Introduce the shared `Meeting` outcome and transaction boundary. +4. Record notification intent transactionally and move delivery behind an adapter. +5. Move direct booking onto the same meeting/notification path. +6. Add workspace authorization when workspace persistence is introduced. +7. Add calendar synchronization through the integration boundary. +8. Introduce import-boundary linting only after module APIs stabilize. + +Avoid a repository-wide folder rewrite. Architecture becomes credible when one complete use case follows the boundaries and is verified. + +## Decisions and deferred mechanisms + +This document recommends notification-as-work-item for the first durable worker. Before implementation, record an ADR if the design changes to a generic outbox. + +Still requiring focused design or prototypes: + +- Physical PostgreSQL meeting-overlap protection +- Worker runtime and deployment trigger +- Lease duration, retry schedule, and dead-letter handling +- Calendar conflict ingestion and webhook reconciliation +- OAuth token encryption/key-management mechanism +- Formal rate-limit storage and thresholds + +## Acceptance checklist + +- [ ] Every mutation enters through a delivery adapter and one application use case. +- [ ] Module ownership and dependency direction are explicit and acyclic. +- [ ] Core business state commits without waiting for external providers. +- [ ] Durable work is written in the same transaction as the state that requires it. +- [ ] Retried booking and finalization commands cannot duplicate outcomes. +- [ ] Authenticated and accountless authorization paths are explicit. +- [ ] Provider adapters cannot redefine internal meeting truth. +- [ ] Expected failures use stable application codes. +- [ ] Logs, lifecycle history, and delivery attempts serve distinct purposes. +- [ ] Tests cover domain, database, provider, and end-to-end boundaries proportionately. + +## Related deployment and delivery plan + +The [deployment architecture](./deployment-architecture.md) maps these runtime boundaries to Vercel, PostgreSQL, worker, provider, and operational concerns. The [roadmap](./roadmap.md) sequences their incremental adoption. diff --git a/docs/use-cases.md b/docs/use-cases.md index d7c88e4..8f9293a 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -962,30 +962,17 @@ These preliminary rules appear across several use cases and will be normalized i - **BR-REL-002:** Retried commands must not create duplicate business outcomes. - **BR-REL-003:** Multi-record lifecycle transitions are atomic where partial state would be invalid. -## Open decisions exposed by the use cases +## Decision outcomes and remaining gates -1. Does direct booking create a `Booking` that is itself the scheduled commitment, or does it create a separate `Meeting`? -2. Does onboarding create a personal workspace immediately? -3. Are open-link and invitation-only polls both supported? -4. How is participant identity represented for accountless users? -5. What is the exact lifecycle state machine for polls and meetings? -6. Which database strategy prevents overlapping active bookings under concurrency? -7. Which actors may cancel or reschedule, and how are accountless actors authorized? -8. Is meeting history modeled as revisions, lifecycle events, or audit records? -9. What durable-job mechanism will support notification and calendar work? -10. When can an automatic finalization policy act on behalf of a host? +Subsequent design work resolved the main structural questions: -These decisions must be addressed before the proposed ER model is treated as accepted. +- Direct booking and poll finalization converge on a shared `Meeting`. +- Every user receives a personal workspace as the ownership boundary. +- Polls support open-link and invitation-only access policies. +- Accountless people use poll/meeting-scoped participant records with secure capability tokens where required. +- Current lifecycle state is paired with append-only lifecycle events. +- Notification intent and delivery attempts are persisted separately from meeting truth. -## Next documentation step - -Create `business-rules.md` to: - -1. Remove duplicate preliminary rules. -2. Give each rule one authoritative definition. -3. Classify rules as current, target, or unresolved. -4. Connect rules to use cases and future tests. -5. Identify which rules require database constraints versus application enforcement. - -The current ER diagram can then be documented accurately, followed by a proposed model tested against these use cases and rules. +Implementation-time gates remain for the physical PostgreSQL overlap mechanism, exact cancellation/rescheduling policies, durable-worker trigger, and automatic-finalization policy. +See [business rules](./business-rules.md), [proposed domain decisions](./domain-decisions.md), [proposed domain model](./domain-model-proposed.md), and the [roadmap](./roadmap.md).