Skip to content

Database

The platform uses Cloudflare D1 (edge SQLite) for all structured data. Schema is defined in db/schema.sql and seed data in db/seed.sql.

TablePurpose
usersAuthenticated users (auto-created on first login). Auth-only: id, email, name, picture, access_level, template IDs. Employee/HR fields have moved to people
tool_permissionsPer-tool, per-access-level permission grants (tools: onboarding, scorecards, templates, objectives, announcements, leave, holidays, people, performance, insights)
org_settingsOrganisation-level key-value settings (timezone, holiday_state)
user_preferencesPer-user preference overrides (timezone, holiday_state)
squadsTeam groupings for data scoping
squad_membersUser-to-squad membership with manager flag
TablePurpose
onboarding_templatesNamed onboarding templates assigned to users
scorecard_templatesNamed scorecard templates assigned to users
scorecard_template_itemsChecklist items belonging to a scorecard template
review_templatesNamed KPI templates for performance reviews
review_template_itemsKPI items per review template (area, label, is_mandatory, sort_order)
TablePurpose
onboarding_phases5 phases with name, description, sort order
onboarding_itemsChecklist items within phases (linked to a template; tpl-onb-common = shown to all users)
onboarding_progressPer-user completion state for each item. person_id links to people for people-centric queries (indexed)
TablePurpose
objectivesBusiness objectives with category (financial/customer/operational/people)
prioritiesCurrent priorities linked to an objective, with is_active flag
kpi_definitionsKPI definitions linked to a priority, scoped by access_level
TablePurpose
scorecardsScorecard header (user, squad, week, type, period_key, notes, comments, scorecard_template_id). person_id links to people (indexed)
scorecard_scoresIndividual score items (status, note)
scorecard_kpisKPI entries (text, target, actual, status, kpi_definition_id)
scorecard_metricsFinancial metrics (billable, utilisation, revenue, profit)
TablePurpose
review_cyclesReview periods (quarterly/annual) with draft/open/closed lifecycle
reviewsOne per user per cycle; status workflow, KPI pass/fail, self-assessment, salary decision. person_id links to people (indexed)
review_kpi_ratingsSnapshot of template items with 3-tier ratings (needs_urgent_work/achieving/exceeding)
review_goalsSMART goals (goal, success measures, support needed, completion status)
feedback_requests360 feedback assignments with pending/submitted/declined status. subject_person_id links to people (indexed)
feedback_responsesStructured 5-area feedback with ratings and comments
review_questionsCentral question bank for reviews and feedback (future use)
TablePurpose
announcementsTitle, content, category (process/team/client/company), attachment URL/name, author
announcement_readsRead receipts per user per announcement
announcement_distributionsDistribution tracking: channel (slack/google_calendar/email), status, external_id, error
TablePurpose
connectionsOrg-level OAuth/API tokens for external services (Xero, Slack, Productive). Tokens are AES-GCM encrypted
user_external_idsDeprecated — external IDs are now stored directly on the people table (xero_employee_id, productive_id). This table is retained for rollback safety and will be removed in a future phase
TablePurpose
leave_typesCached from Xero PayItems (leave type ID, name, is_paid)
leave_balancesPer-user leave balances cached from Xero (1hr TTL). person_id links to people (indexed)
leave_requestsLeave requests with status, dates, approval chain, external integration IDs, and error tracking. person_id links to people (indexed)
leave_request_historyAudit trail of all actions (submitted, approved, rejected, cancelled)
TablePurpose
public_holidaysCached Australian public holidays from Nager.Date API (24hr TTL)
TablePurpose
peopleSource of truth for all employee/HR data. Synced from Productive (name, email, title, avatar, manager hierarchy) and enriched via Xero sync (employment, contact, demographics, financial fields). People do not require an app user account
TablePurpose
pay_summariesCached pay template info from Xero (annual salary, earnings YTD, next pay date). person_id links to people (indexed, unique)
payslipsCached payslip summaries from Xero. person_id links to people (indexed)

The platform is migrating from user-centric to people-centric data ownership:

  • Phase 1: Employee/HR fields (employment, contact, demographics, financial) moved from users to people. The users table is now auth-only. Profile and insights queries read from people.
  • Phase 2: Added person_id foreign keys to 8 employee data tables (pay_summaries, payslips, leave_balances, leave_requests, scorecards, onboarding_progress, reviews, feedback_requests). Existing rows backfilled via users.person_id join. Auth middleware resolves personId on every request. API routes use a dual-write pattern (both user_id and person_id) during the transition period. Indexed for query performance.
  • Phase 3: Eliminated user_external_ids dependency. All external ID lookups (Xero, Productive) now read directly from people table columns (xero_employee_id, productive_id). Removed dual-writes to user_external_ids from Xero linking, Productive sync, and payroll sync. Admin mappings UI now shows all people and writes directly to people. The user_external_ids table is retained for rollback safety.
  • Phase 4: Added getScopedPersonIds() using people.manager_id hierarchy for data scoping. All route scoping across scorecards, onboarding, leave, and performance now uses WHERE person_id IN (...) instead of WHERE user_id IN (...). Managers see direct reports via the people hierarchy. List queries JOIN people for name/email instead of users. The old getScopedUserIds() (squad-based) is retained for backward compatibility.
  • Phase 5: Cleanup and removal of dual-writes. Dropped user_external_ids table. Deleted getScopedUserIds(). Migrated all WHERE user_id = ? data queries to WHERE person_id = ? in performance, onboarding, and leave routes. Removed user_id from INSERT statements in reviews, scorecards, onboarding_progress, and leave_requests. Leave approval ownership checks use person_id instead of user_id, and manager verification uses people.manager_id hierarchy instead of squad_members. Leave integrations resolve external IDs via people.id instead of people.user_id. Removed backward-compat users table write from Xero payroll sync. Added partial unique indexes on person_id for ON CONFLICT migration.
  • Phase 6: Migrated pay_summaries, payslips, and leave_balances reads from WHERE user_id to WHERE person_id. Backfilled null person_id values. Added UNIQUE(person_id, leave_type_id) index for leave_balances ON CONFLICT migration. refreshPayrollData now takes personId directly. Replaced squad_members usage in insights (span of control, manager count, team sizes) and performance (feedback peer selection) with people.manager_id hierarchy. Scorecards squad filter uses people.squad column. People profile pay/leave lookups use person_id directly.
  • Phase 7: Final cleanup. Recreated all 8 data tables via SQLite table recreation to drop user_id columns. person_id is now NOT NULL REFERENCES people(id) ON DELETE CASCADE on all data tables. Changed pay_summaries PK from user_id to person_id. Removed subject_id from feedback_requests (uses subject_person_id only). Unique constraints moved from user_id combos to person_id combos (inline, non-partial). Dropped squad_members table — squad membership now derived from people.squad column with idx_people_squad index. Admin squad management and squad filter routes rewritten. Dropped all stale user_id indexes.
Terminal window
# Apply schema locally
pnpm db:migrate
# Apply seed data locally
pnpm db:seed
# Apply to remote (production)
wrangler d1 execute nucleus-db --remote --file=db/schema.sql
wrangler d1 execute nucleus-db --remote --file=db/seed.sql