CODITECT
CODITECT VTR
Visual Test Report
PASSED

SDD v2.0 — Software Design Document

Source: sdd-v2.md

Software Design Document

Group Availability Scheduling Tool

Version: 2.1.0 — Cloudflare-native replatform

Date: 2026-04-11

Classification: Internal — CODITECT Platform Artifact A4

Status: Draft — supersedes v1.0.0

Owner: Platform Engineering

Change summary: Full replatform from Node.js/PostgreSQL/Redis to Cloudflare Workers · D1 · KV · Durable Objects · Pages · R2 · Email Workers. All functional requirements unchanged. Non-functional requirements updated for Cloudflare edge runtime. New sections: §5.4 Cloudflare Runtime Constraints, §11.3 Email Workers. | v2.1: Contact capture (email), Google Calendar event creation on poll close, VCF/CSV/JSON contact export, CODITECT CRM integration.

---|---------|

Meeting organizerCreate polls quickly; see aggregated results
ParticipantRespond without account creation
Platform adminData retention, privacy compliance
Security leadNo PII leakage, link hijack prevention

2. System Context

2.1 C4 Context

[Organizer]     ──creates poll──►  [CF Pages SPA]  ──fetch /api/polls──►  [CF Worker]
[Participant]   ──responds via──►  [CF Pages SPA]  ──fetch /api/responses─►  [CF Worker]
[Any browser]   ──WebSocket──────►  [CF Pages SPA]  ──WSS upgrade──────────►  [CF Worker]
                                                                                    │
[CF Worker] ──D1 SQL──────►  [Cloudflare D1]
[CF Worker] ──KV get/put──►  [Cloudflare KV]
[CF Worker] ──DO fetch──────►  [Durable Object: PollHub]
[CF Worker] ──R2 put──────►  [Cloudflare R2]
[CF Worker] ──fetch──────────►  [MailChannels API]  (notifications only)
[CF Cron]   ──scheduled()──►  [CF Worker]  ──D1──►  [Cloudflare D1]

2.2 Cloudflare Primitive Map

Concernv1.0 (Node.js)v2.0 (Cloudflare)
Frontend hostingVercel / RailwayCloudflare Pages
API computeNode.js Express / HonoCloudflare Workers (Hono)
Relational DBPostgreSQL 16Cloudflare D1 (SQLite)
Key-value / rate limitRedis 7Cloudflare KV
Real-time WebSocket`ws` npm + Redis pub/subDurable Objects (hibernation API)
Edge cacheCloudFront / CDNCloudflare Cache API
Object storageS3Cloudflare R2
EmailNodemailer + SESEmail Workers + MailChannels

3. Functional Requirements (unchanged from v1.0)

3.1 Poll Management

IDRequirementPriority
FR-01Organizer can create a poll with title, description, timezone, 1–30 candidate slotsP0
FR-02Each slot has a date and session label (Morning / Afternoon / custom)P0
FR-03System generates a unique shareable URL per pollP0
FR-04Organizer receives a private management URL + PIN to edit/close pollP0
FR-05Poll has configurable expiry (default 14 days)P1
FR-06Organizer can mark one slot as "selected" to close the pollP1
FR-07Organizer can add optional notes per slotP2
FR-08Organizer can provide email for calendar event creationP1

3.2 Participant Response (unchanged)

IDRequirementPriority
FR-10Participant enters display name and toggles available/unavailable per slotP0
FR-11Participant can mark slots as "if needed" (tentative)P1
FR-12Participant can edit response via browser cookie tokenP1
FR-13Participant count and slot heatmap visible in real timeP1
FR-14System warns on duplicate name (not a hard block)P2
FR-15Participant can optionally provide email for calendar inviteP1

3.3 Results & Recommendation (unchanged)

IDRequirementPriority
FR-20System ranks slots by available countP0
FR-21System flags top slot as BEST when count ≥ 50% of respondentsP0
FR-22Organizer can export results as CSVP1
FR-23Organizer can export draft calendar invite (.ics)P1
FR-24System notifies organizer when poll closes/expiresP2
FR-30System creates Google Calendar event with selected slot when poll closesP1
FR-31System adds participants with email as calendar attendeesP1
FR-32System sends calendar invites via Google WorkspaceP1
FR-33Organizer can export participant contacts as VCFP1
FR-34Organizer can export participant contacts as CSVP1
FR-35Organizer can export participant contacts as JSONP1
FR-36Contacts sync to CODITECT CRM on poll closeP2

4. Non-Functional Requirements (v2.0 updated)

IDCategoryRequirementTargetNotes
NFR-01PerformancePoll page FCP< 1.0s p95CF edge serving
NFR-02PerformanceResponse submission RTT< 300ms p99Worker at edge
NFR-03ScalabilityConcurrent active polls100,000D1 + DO scale independently
NFR-04ScalabilityParticipants per poll500DO WS limit per instance
NFR-05AvailabilityUptime SLA99.9%Cloudflare global network
NFR-06PrivacyNo PII beyond display nameRequiredADR-005 unchanged
NFR-07PrivacyPoll data purged after expiry + 30 daysRequiredCron trigger
NFR-08Security128-bit random slugsRequiredcrypto.getRandomValues
NFR-09SecurityRate limiting on response submissionRequiredKV sliding window
NFR-10AccessibilityWCAG 2.1 AARequiredTailwind color tokens
NFR-11I18NTimezone-aware; UTC storageRequiredINTEGER timestamps in D1
NFR-12RuntimeCloudflare Workers runtime onlyRequiredNo Node.js APIs

5. Architecture Overview

5.1 Architectural Style

Cloudflare-native edge SPA. Static frontend on Cloudflare Pages (React + Vite). All API logic in a Cloudflare Worker (Hono router). Persistent state in D1 (SQLite), ephemeral state in KV. Real-time via Durable Objects using the WebSocket hibernation API. No long-running server processes.

5.2 Layer Diagram

┌──────────────────────────────────────────────────┐
│  Cloudflare Pages (React 19 + Vite)              │
│  Home · Poll · Manage · Tailwind · WCAG 2.1 AA   │
└───────────────────┬──────────────────────────────┘
                    │ HTTPS fetch /api/* · WSS /api/polls/:slug/ws
┌───────────────────▼──────────────────────────────┐
│  Cloudflare Worker (Hono router)                 │
│  polls.ts · responses.ts · results.ts            │
│  manage.ts · ws.ts · health.ts                   │
│  rate-limit (KV) · security headers · CORS       │
└────┬──────────┬──────────┬───────────┬───────────┘
     │          │          │           │
 ┌───▼───┐  ┌──▼──┐  ┌────▼───┐  ┌───▼────────────┐
 │  D1   │  │ KV  │  │  R2    │  │ Durable Object │
 │SQLite │  │rate │  │exports │  │  PollHub WS    │
 │polls  │  │lock │  │staging │  │  fan-out       │
 └───────┘  └─────┘  └────────┘  └────────────────┘
                                        │
                                  ┌─────▼──────────┐
                                  │ MailChannels   │
                                  │ (notifications)│
                                  └────────────────┘

5.3 Key Design Decisions

See ADRs: 001 (Next.js → Cloudflare Pages/Workers), 002 (auth model), 003 (DO WebSocket), 004 (Drizzle D1), 005 (data retention), 006 (D1 vs PostgreSQL).

5.4 Cloudflare Runtime Constraints (v2.0 NEW)

These constraints apply to every Workers file and must not be violated:

ConstraintDetail
No `bcrypt`Workers have no native module support. Use PBKDF2 via `crypto.subtle`
No Node.js built-insNo `fs`, `path`, `os`, `net`, `crypto` (Node) — use Web Crypto API
No `ws` npm libraryWebSockets via `WebSocketPair` + Durable Objects only
No persistent memoryWorkers are stateless per request. All state in D1/KV/DO
No `setTimeout` for deferred workUse Durable Object alarms or Cron Triggers
ES modules onlyNo `require()` — import/export everywhere
D1 is SQLiteNo `gen_random_uuid()`, no PostgreSQL extensions, no `RETURNING`
DO hibernationAlways use `ctx.acceptWebSocket()` — never legacy `ws.accept()`
30s CPU limitWorker execution must complete within 30 seconds
128MB memory limitNo large in-memory datasets — stream large outputs

6. Component Design (v2.0 updated)

6.1 PollService (D1)

Same interface as v1.0. Implementation changes: uses Drizzle D1 adapter. ID generation via crypto.randomUUID() in application code (not DB). Timestamps as Unix integer seconds.

6.2 ResponseService (D1 + KV + DO)

After each successful D1 write: (1) invalidate Cache API results key, (2) call ctx.waitUntil(do.broadcast(payload)) — fire-and-forget DO broadcast.

6.3 RecommendationEngine (unchanged)

Pure function — no I/O. score = availableCount + tentativeCount * 0.5. isBest = availableCount >= totalRespondents * 0.5. Tie-break by earliest date.

6.4 PollHub Durable Object

One DO instance per poll, keyed by env.POLL_HUB.idFromName(slug). Uses hibernation API — DO sleeps between messages, waking only to broadcast. No application state stored in DO memory — all data in D1. broadcast() method serializes payload and sends to all connected WebSocket clients via ctx.getWebSockets().


7. Data Model (v2.0 — D1/SQLite)

7.1 Schema changes from v1.0

  • UUIDTEXT (SQLite has no native UUID type)
  • TIMESTAMPTZINTEGER (Unix seconds; timezone handled in application layer)
  • gen_random_uuid() removed — IDs generated in application code
  • RETURNING clause removed — query after insert
  • bcrypt_hash split into pin_hash + pin_salt (PBKDF2 requires explicit salt storage)

7.2 Schema

CREATE TABLE polls (
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,
  organizer_pin_hash TEXT NOT NULL,
  organizer_pin_salt TEXT NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  timezone TEXT NOT NULL DEFAULT 'UTC',
  status TEXT NOT NULL DEFAULT 'OPEN',
  selected_slot_id TEXT,
  expires_at INTEGER NOT NULL,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  organizer_email TEXT NOT NULL DEFAULT '',
  calendar_event_id TEXT DEFAULT '',
  google_meet_link TEXT DEFAULT ''
);

CREATE TABLE slots (
  id TEXT PRIMARY KEY,
  poll_id TEXT NOT NULL REFERENCES polls(id) ON DELETE CASCADE,
  slot_date TEXT NOT NULL,
  session_label TEXT NOT NULL,
  start_time TEXT,
  end_time TEXT,
  notes TEXT,
  sort_order INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE responses (
  id TEXT PRIMARY KEY,
  poll_id TEXT NOT NULL REFERENCES polls(id) ON DELETE CASCADE,
  display_name TEXT NOT NULL,
  edit_token_hash TEXT NOT NULL,
  edit_token_salt TEXT NOT NULL,
  email TEXT NOT NULL DEFAULT '',
  version INTEGER NOT NULL DEFAULT 1,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

CREATE TABLE slot_responses (
  id TEXT PRIMARY KEY,
  response_id TEXT NOT NULL REFERENCES responses(id) ON DELETE CASCADE,
  slot_id TEXT NOT NULL REFERENCES slots(id) ON DELETE CASCADE,
  status TEXT NOT NULL CHECK(status IN ('AVAILABLE','IF_NEEDED','UNAVAILABLE')),
  UNIQUE(response_id, slot_id)
);

CREATE INDEX idx_polls_slug ON polls(slug);
CREATE INDEX idx_polls_expires_at ON polls(expires_at) WHERE status = 'OPEN';
CREATE INDEX idx_slots_poll_id ON slots(poll_id);
CREATE INDEX idx_responses_poll_id ON responses(poll_id);
CREATE INDEX idx_slot_responses_slot_id ON slot_responses(slot_id);

8. API Design (unchanged endpoints, v2.0 implementation notes)

MethodPathAuthCache TTL
POST`/api/polls`None
GET`/api/polls/:slug`None60s (Cache API)
GET`/api/polls/:slug/results`None5s (Cache API)
POST`/api/polls/:slug/responses`None— (invalidates results cache)
PATCH`/api/polls/:slug/responses/:id`Edit token cookie
POST`/api/polls/:slug/close`PIN (KV lockout)
GET`/api/polls/:slug/export/csv`PIN
GET`/api/polls/:slug/export/ics`PIN
GET`/api/polls/:slug/export/contacts`PINVCF contact export
GET`/api/polls/:slug/export/contacts.csv`PINCSV contact export
GET`/api/polls/:slug/export/contacts.json`PINJSON contact export
GET`/api/polls/:slug/ws`None→ DO upgrade
GET`/api/health`Noneno-store

9–10. UI/UX Design & Security Design (v2.0)

9.1 UX Changes from v1.0

  • Mobile-first Tailwind v4 throughout
  • Slot grid: dates as columns desktop, stacked cards mobile
  • Name entry step gates access to the slot grid (animated transition)
  • Three-state cell toggle: Available → If Needed → Unavailable (tap cycles)

10.1 Security Changes (v2.0)

  • bcrypt replaced by PBKDF2 (100k iterations, SHA-256) — same security model, Web Crypto API compatible
  • KV used for rate limiting and PIN lockout — Redis no longer required
  • Edit token: PBKDF2 hash + separate salt column in D1 (replaces bcrypt)

11. Integration Points (v2.0)

11.1 Real-Time Updates (Durable Objects)

Replaced Redis pub/sub with Durable Object hibernation API. Single DO instance per poll. Worker calls ctx.waitUntil(doStub.broadcast(payload)) — fire-and-forget from the critical path.

11.2 Email (Email Workers + MailChannels)

Replaced Nodemailer + SES with Cloudflare Email Workers + MailChannels. POST to https://api.mailchannels.net/tx/v1/send with JSON body. Free tier for Cloudflare Workers. No SMTP credentials. DKIM signing handled by Cloudflare.

11.3 File Export

CSV streamed via TransformStream — no full-file buffering in memory (Workers 128MB limit). ICS generated as string (small enough). Both returned as direct download responses. R2 not required for v2.0 (files fit within Workers response limits).

11.4 Google Calendar Integration (v2.1)

When organizer closes a poll with a selected slot, the Worker calls the Google Calendar API via a service account (impersonating 1@az1.ai) to:

1. Create a calendar event with the selected slot's date/time, poll title, and description

2. Add all participants who provided email as attendees

3. Google auto-sends calendar invite emails to all attendees

4. Store the calendar_event_id and google_meet_link in the polls table

The calendar event creation is non-blocking — if Google Calendar API fails, the poll still closes successfully. The error is logged but not surfaced to the user.

Service account key stored as GOOGLE_SERVICE_ACCOUNT_KEY Cloudflare secret. JWT signed with Web Crypto API (RS256) since google-auth-library is not available in Workers runtime.


12. Deployment Architecture (v2.0)

Frontend:   Cloudflare Pages (auto-deploy from git, global CDN)
Worker:     Cloudflare Workers (global edge, wrangler deploy)
Database:   Cloudflare D1 (managed SQLite, automatic replication)
KV:         Cloudflare KV (global, eventual consistency — acceptable for rate limiting)
DO:         Cloudflare Durable Objects (strong consistency per instance)
R2:         Cloudflare R2 (available for future large export needs)
Cron:       Cloudflare Cron Triggers (defined in wrangler.toml)

12.1 wrangler.toml summary

[[d1_databases]] binding = "DB"
[[kv_namespaces]] binding = "KV"
[[r2_buckets]] binding = "R2"
[durable_objects] bindings = [{name="POLL_HUB", class_name="PollHub"}]
[[triggers]] crons = ["0 * * * *", "0 2 * * *"]

13. Open Issues (v2.0 updated)

IDIssueStatus
OI-01~~Real-time strategy~~✅ Resolved — Durable Objects (ADR-003 updated)
OI-02"If Needed" state in v1.0Open — include in v2.0
OI-03CAPTCHA on poll creationOpen
OI-04Organizer email optional/requiredDecision pending
OI-05Multi-languageDeferred
OI-06D1 row limits for very large polls (500 participants × 8 slots = 4,000 rows)Assessed — within D1 limits

*CODITECT Artifact A4 — SDD v2.0.0 · Group Availability Scheduling Tool · Cloudflare-native*

*Generated: 2026-04-09 · Status: Draft · Supersedes: v1.0.0*

*[LEGAL REVIEW REQUIRED] for EU/BR production launch per ADR-005*