Overview

MyCirclee is a native iOS application built entirely with SwiftUI and backed by Supabase. Zero third-party SDKs beyond Supabase itself, zero custom servers, and zero tracking frameworks.

~34 View Files
~15 Services
14 DB Tables
0 3rd-Party SDKs

System Architecture

A fully serverless architecture. The iOS app communicates exclusively through the Supabase client SDK, which manages auth, database queries, real-time WebSocket subscriptions, and edge function invocations. No custom backend server exists.

Client
Server
Data
Service
External
End-to-End System Flow
iOS App SwiftUI + @Observable
Supabase Client SDK Auth, DB, Realtime, Storage
Supabase Platform
Auth (JWT)
PostgreSQL + RLS
Realtime (WebSocket)
Edge Functions → APNs

100% serverless. There is no Express server, no Lambda, no Docker container. Every backend capability is provided by Supabase's managed platform, including auth, database, real-time pub/sub, and edge functions for push notifications.


Tech Stack Layers

Five distinct layers from the user interface down to the backend infrastructure. Each layer has clearly bounded responsibilities.

Application Layer Stack
UI Layer
SwiftUI Views WidgetKit Live Activities Theme System
State Layer
AppState (@Observable) SwiftData UserDefaults Keychain
Service Layer
Realtime Subscriptions Push Notifications CoreSpotlight Rich Haptics Location
Infrastructure
Supabase SDK URLSession CoreLocation MapKit CoreHaptics
Backend
PostgreSQL Supabase Auth Realtime Engine Edge Functions APNs

AppState: The Brain

A single @Observable class (~2,800 lines) acts as the centralized state manager. Injected via @Environment into every view. Handles all data fetching, caching, subscriptions, and mutations in one place.

SwiftData: Local Persistence

On-device persistence layer backed by SwiftData. Stores cached data, draft posts, user preferences, and widget configuration. Enables offline reads and fast cold launches.

RichHaptics: Tactile Feedback

Custom haptic engine combining CoreHaptics patterns with UIKit feedback generators. Every interaction (reactions, status changes, polls) has purpose-designed haptic feedback.

CoreSpotlight: System Search

Posts and contacts are indexed via CoreSpotlight for iOS system-wide search. All indexing happens on-device. Nothing is sent to any server for search purposes.


Database Schema

14 PostgreSQL tables organized into four logical groups. Every table is protected by Row Level Security policies that enforce circle-level data isolation.

Core

circles

id name created_by created_at invite_code

circle_members

circle_id user_id role joined_at

profiles

user_id display_name handle bio dm_privacy is_pro full_name location address email phone birthday
Content

posts

id circle_id author_id type body mood category urgency location latitude longitude photo_url expires_at created_at

post_reactions

post_id user_id emoji created_at

poll_options

id post_id label created_at

poll_votes

option_id user_id created_at

event_rsvps

post_id user_id status created_at
Messaging

dm_threads

id circle_id user_a user_b last_message_at notify_a notify_b

dm_messages

id thread_id sender_id body reply_to_id created_at read_at

dm_reactions

message_id user_id emoji created_at
System

presence_states

user_id circle_id status updated_at

push_tokens

user_id token platform created_at

kept_posts

user_id post_id kept_at

add_requests

id circle_id from_user_id to_user_id status created_at

Key relationships: All foreign keys enforce referential integrity. circle_members is the join table that gates every RLS policy. A user can only read rows where their user_id appears in circle_members for the relevant circle_id.

circles.id circle_members.circle_id circles.id posts.circle_id posts.id post_reactions.post_id posts.id poll_options.post_id poll_options.id poll_votes.option_id posts.id event_rsvps.post_id dm_threads.id dm_messages.thread_id dm_messages.id dm_reactions.message_id profiles.user_id presence_states.user_id posts.id kept_posts.post_id

Data Flow: Post Creation

The lifecycle of a post from composition to automatic expiry. Optimistic updates provide instant feedback while the server sync happens in the background.

Post Lifecycle
1 User taps compose — Opens the compose sheet with post type picker (note, event, ask, poll)
2 Selects post type — Form adapts: events get date/location, polls get option fields, asks get urgency levels
3 Fills form + drafts auto-saved — Content persists to UserDefaults so nothing is lost on accidental dismiss
4 Taps publish — On-device content safety check runs. Haptic feedback confirms the tap
5 Optimistic UI update — Post appears immediately in the local feed before server response. Spinner indicates sync in progress
6 Supabase INSERT — Row inserted into posts table. RLS validates the author is a circle member. expires_at set to now() + 24h
7 Realtime broadcast — Supabase Realtime detects the INSERT and pushes the event to all subscribed clients in the same circle
8 Edge Function triggers push — Deno edge function queries push_tokens for circle members and dispatches APNs notifications
9 Other clients receive — WebSocket event decoded, client-side filter confirms circle match, AppState updated, SwiftUI re-renders
10 24h later: server deletes — Scheduled server-side process deletes rows where expires_at < now(). Client also filters locally for instant UI consistency

Data Flow: Realtime Subscriptions

Three persistent WebSocket channels keep the UI synchronized across all devices in a circle. Supabase Realtime handles the pub/sub layer; RLS enforces security at the database level.

Subscription Lifecycle
1 App launches + user authenticates — JWT token obtained from Supabase Auth, stored in Keychain
Posts Channel insert / update / delete
DMs Channel messages + reactions
Presence Channel status changes
2 WebSocket connection maintained — Single persistent connection multiplexes all three channels. Auto-reconnects on network change
3 On event: decode payload — Incoming JSON decoded into Swift model structs. Unfiltered subscription; client performs circle-level filtering
4 Filter by active circle — Only events matching the user's current circle ID pass through. Stale or foreign events are discarded
5 Update AppState + UI re-renders@Observable property changes trigger automatic SwiftUI view updates. No manual refresh needed

Why unfiltered subscriptions? Supabase Realtime applies RLS to prevent unauthorized access at the database layer. The client then applies a secondary circle-level filter for DMs to show only the active conversation. This two-layer approach ensures security without sacrificing flexibility.


Data Flow: Widget Architecture

Three WidgetKit extensions (small, medium, lock screen) plus ActivityKit Live Activities. Widgets and the host app communicate through a shared App Group container.

Widget Communication Pipeline
1 App writes to App Group container — Presence data, upcoming events, and user preferences serialized to shared UserDefaults
Small Widget Presence dots
Medium Widget Status buttons
Lock Screen Compact presence
2 Widget reads shared data — WidgetKit timeline provider reads from App Group container to build entries
3 User taps widget status button — Medium widget exposes interactive buttons for status changes (Free, Deep Work, Out, etc.)
4 WidgetKit Intent fires — App Intent handler activates, reads the desired status from the intent parameters
5 Supabase UPDATEpresence_states row updated. Realtime broadcasts the change to all circle members
6 WidgetCenter.reloadTimelines() — Host app signals widgets to refresh. All three widget families update simultaneously

Live Activities

ActivityKit powers lock-screen Live Activities for upcoming events. Shows countdown, location, and RSVP count. Updates via push token delivery from the edge function.

Timeline Strategy

Widgets use an atEnd reload policy with a 15-minute cadence. Presence data updates immediately via intent-triggered reloads; background refreshes fill gaps.


Authentication Flow

Two authentication methods: traditional email + password and passwordless magic link. Both managed entirely by Supabase Auth with JWT tokens stored in the iOS Keychain.

Auth Lifecycle
1 Email input — User enters email on the auth screen. Picks between password sign-in or magic link
2 Supabase Auth — Credentials validated server-side. For magic link: one-time token emailed. For password: bcrypt hash compared
3 JWT issued — Access token (short-lived) + refresh token (long-lived) returned to the client
4 Stored in Keychain — Tokens persisted in the iOS Keychain (hardware-encrypted, survives app reinstalls on same device)
5 Auto-refresh — Supabase SDK transparently refreshes expired access tokens using the refresh token. No user interaction needed
6 All API calls authenticated — Every database query includes the JWT in the Authorization header. RLS policies extract auth.uid() to enforce access
7 RLS enforces circle isolationWHERE circle_id IN (SELECT circle_id FROM circle_members WHERE user_id = auth.uid())

Security Architecture

Defense in depth: every layer from the database up to the UI enforces data isolation. No single point of failure can expose cross-circle data.

Row Level Security

Every table has RLS policies enabled. Queries are automatically filtered by circle membership. Even direct SQL access to the database respects these boundaries.

Auth Token Management

JWTs are stored in the iOS Keychain, never in UserDefaults or plain files. The Supabase SDK handles transparent token refresh without exposing credentials to app code.

On-Device Content Safety

Content safety analysis runs entirely on-device using Apple's frameworks. No post content is ever sent to an external service for scanning or moderation.

Ephemeral by Design

Posts auto-delete after 24 hours at the database level. This is not a soft archive or hidden flag. The data is permanently removed and unrecoverable.

Invite Security

Invite codes are single-use and expire after 48 hours. Once claimed, the code is invalidated. No link can be shared publicly to grant ongoing access.

DM Privacy Levels

Users control who can DM them with three levels: anyone in the circle, reciprocal only (both must opt in), or nobody. Enforced at the database query layer.

Security Boundary Model
Internet Untrusted
TLS Encryption Transit layer
Supabase Auth JWT validation
RLS Policies Row-level filtering
Client Filter Circle-scoped

Design System

Centralized in Theme.swift, the design system defines every visual token: colors, typography, spacing, shadows, and interaction patterns. Consistency is enforced through SwiftUI view modifiers rather than ad-hoc styling.

Color Palette

Warm Amber
#D0845E
Near Black
#1A1612
Warm White
#F4EDE4
Free (Green)
#5AB87A
Deep Work (Purple)
#9B7AD8
Out (Orange)
#D4913A
Still Up (Indigo)
#7B8FD4
Asleep (Gray)
#6B6B6B

Typography Pairing

Display / Headings
Instrument Serif
System serif used for page titles, section headings, and hero text. Warm, editorial feel that matches the app's intimate tone.
Body / UI
SF Pro (Inter on web)
System sans-serif for body copy, buttons, labels, and all interactive elements. Clean and highly legible at small sizes.

Component Patterns

.warmCard()
Consistent card styling with warm tones, subtle border, and shadow.
PressableButtonStyle
Tap me
Presence Dots
Post Type Chips
Note Event Ask Poll

Haptic Design Language: RichHaptics.swift maps every user action to a specific haptic pattern using CoreHaptics and UIKit generators. Reactions use a soft impact, status changes use a double-tap pattern, and errors use a notch feedback. Haptics are never random or decorative.


Data Flow Patterns

Five core patterns govern how data moves through the application. Each pattern is purpose-built for its use case.

Optimistic Updates

Reactions, RSVPs, and poll votes update the UI immediately before the server confirms the mutation. If the server rejects the change, the UI rolls back. This makes interactions feel instant.

Realtime Subscriptions

Unfiltered WebSocket subscriptions with client-side circle filtering. Supabase RLS prevents unauthorized access at the database layer; the client narrows to the active context.

Ephemeral Expiry

Server-side scheduled deletion of posts where expires_at < now(). The client also filters expired posts locally for instant consistency, even before the server cron runs.

Draft Persistence

The compose view auto-saves drafts to UserDefaults as the user types. Drafts survive app kills, crashes, and accidental dismissals. Restored silently on next compose.

Widget Communication

App Groups shared container bridges the host app and widget extensions. The app writes presence data and event info; widgets read it. WidgetCenter.reloadTimelines() signals refresh. Interactive widgets fire App Intents that flow back through the host app to Supabase.