Architecture
┌──────────────┐ ┌──────────────────┐ ┌───────────────────────────┐
│ apps/web │ │ apps/android │ │ apps/ios (SwiftUI) │
│ Next.js │ │ Expo / RN │ │ Liquid Glass UI │
│ React 19 │ │ + RN widget │ │ + WidgetKit / Live Act. │
└──────┬───────┘ └───────┬──────────┘ └────────────┬──────────────┘
│ @omni/api, @omni/core, @omni/i18n │ supabase-swift
└──────────────┬─────────────┘ │
▼ ▼
┌──────────────────────────────────────────────┐
│ Supabase │
│ Auth (email, OAuth, SAML SSO) │
│ Postgres + PostGIS + RLS (org isolation) │
│ RPC: clock, clock_status, create/redeem │
│ Realtime (DMs, presence, live attendance) │
│ Storage (documents, avatars, attachments) │
│ Edge Functions (push-dispatch, exports) │
└──────────────────────────────────────────────┘
Packages
| Package | Purpose |
|---|---|
@omni/design-tokens |
The source of truth for the RSQ.sa brand. tokens.json is turned into CSS variables, a Tailwind v4 @theme, a React Native theme, and Swift OmniTheme.swift. It also ships the HT Moshreq Pro font files. |
@omni/core |
Pure TypeScript domain logic: zod schemas, computeDay() (worked / late / overtime against a shift), the geofence check, EOSB, and invite-code generation. Used by web and Android; the logic is mirrored in SQL for server-side authority. |
@omni/i18n |
Arabic and English dictionaries, plus dir() and number/date formatting with Hijri support. |
@omni/api |
Supabase client factory, generated Database types, and typed wrappers for the RPCs. |
Multi-tenancy & security
- Every business table has an
org_id. RLS policies useprivate.current_org(),private.is_member()andprivate.has_role(...)(SECURITY DEFINER helpers in a non-exposed schema), which read fromorg_membersfor the callingauth.uid().- The active org is stored in
profiles.active_org_id. A user can belong to several orgs and switch between them.
- The active org is stored in
- Roles:
owner > admin > hr > manager > employee. Managers see their reports throughemployees.manager_id. - Clients never insert attendance events directly. Only the
SECURITY DEFINERRPCpublic.clock(...)writes them, after it has:- resolved the employee's effective policy and shift
- checked the geofence (
ST_DWithinongeography) and the GPS accuracy - de-duplicated by
idempotency_key - set
statustook,flaggedorrejectedwith areason
Check-in from widgets
The widget can't open the app, so the token and the API call have to live outside the app's UI process.
iOS (SwiftPM + xtool, see apps/ios/README.md)
- The app keeps the Supabase session in its own Keychain and writes the last
omni_clock_statussnapshot to the App Groupgroup.sa.rsq.omni. - Every widget, Live Activity, Control Center and Siri action is a
LiveActivityIntent. iOS runs it in the app process without bringing the app to the foreground. The intent:- gets one location fix with
CLLocationUpdate(when the policy needs it) - calls
rpc/omni_clock - starts or ends the Live Activity
- reloads widget timelines
- gets one location fix with
- The widget extension never holds auth tokens. Its timelines render the App Group snapshot with
Text(timerStart, style: .timer), so the elapsed time ticks without reloads.
Android
- The
react-native-android-widgettask handler runs headless JS when the button is tapped. It:- reads the session from
expo-secure-store - gets a location with
expo-location(getCurrentPositionAsync, balanced accuracy) - calls
clockthrough@omni/api - re-renders the widget
- reads the session from
- While the user is checked in, a Notifee foreground-service notification shows a native Chronometer.
- The Quick Settings tile is a small Kotlin Expo module that sends a broadcast to the same headless task.
Notifications
push_tokens(user_id, platform, token)are registered by the apps.- Events go into
notifications(the in-app inbox). A DB webhook triggers thepush-dispatchEdge Function, which sends to APNs (token-based auth) and FCM HTTP v1, honoring each user's preferences and quiet hours. - Scheduled reminders ("shift starts in 10 min", "missed check-out") run in
pg_cronand insert notifications.
SSO
- Supabase Auth SAML 2.0 providers are registered per org domain (
supabase sso add). The org admin UI stores the metadata URL and domains inorg_sso_domains. - The sign-in screen looks up the email domain and calls
signInWithSSO({ domain }). - On first SSO login, the user is auto-joined to the org that owns the domain, with the default role
employee.
Repos & tooling
- pnpm workspaces + Turborepo, Biome (lint and format), Vitest (unit), and Playwright (web e2e).
- The Supabase CLI handles local DB, migrations and type generation.
- iOS is a SwiftPM package built with xtool (
apps/ios/Package.swift+xtool.yml). Swift tokens are generated from@omni/design-tokens. - Hosting: the web app on Vercel, the backend on Supabase. EAS builds Android; Xcode Cloud or fastlane builds iOS.
Data residency
Supabase-hosted regions closest to KSA are me-central-1 (UAE) and eu-central-1. If a customer needs PDPL in-Kingdom residency, the same stack can be self-hosted in a Saudi cloud region; nothing in the code depends on Supabase-hosted-only features, apart from SAML, which self-hosted GoTrue also supports.
Database
| Migration | Contents |
|---|---|
20261027000001_core_schema.sql |
Enums, orgs, profiles, members, departments, sites (PostGIS plus generated lat/lng), employees, shift templates, attendance policies, assignments, attendance events, invites, SSO domains |
20261027000002_rls.sql |
private.* membership helpers, RLS on every table, column-level update grants on attendance, and the people_directory view (no salary or IDs) |
20261027000003_rpc.sql |
New-user trigger (profile plus SSO domain auto-join), my_context, set_active_org, create_organization, create_invite, redeem_invite, clock_status, clock, and the policy/shift resolver private.effective_assignment |
20261027000004_notifications.sql |
push_tokens, notifications, and a trigger that notifies the manager about a flagged check-in |
Policy resolution: the most specific active assignment wins, in this order: employee › department › site › company. Within a scope, an assignment with a start date beats an open-ended one, so date-bounded presets like Ramadan apply automatically.
Tests: supabase/tests/run-local.sh sets up a throwaway Postgres + PostGIS with a minimal auth shim, then applies the migrations and seed and runs supabase/tests/*_test.sql. The tests cover:
- org isolation
- salary privacy
- invite redemption
- the clock state machine and idempotency
- strict, flexible and off geofencing
- late-sync flagging
- tamper protection
- policy precedence