A real-time social status app where you can share what you're up to with your friends. Think of it as a lightweight, privacy-focused "what are you doing right now?" feed.
- Rez (Rezonate)
Rez lets you:
- Post a short status (up to 42 characters) so friends know what you're up to
- See your friends' statuses and when they last updated them
- Send, accept, and reject friend requests by username
- Reorder your friends list via drag and drop
- Pick from quick-select status presets you define yourself
- Choose from 35 DaisyUI themes
| Feature | Details |
|---|---|
| Authentication | Email/password with email confirmation, password reset, email change |
| Status updates | 42-character limit, with quick-status presets |
| Friend requests | Send by username, accept/reject/cancel |
| Real-time sync | Supabase Realtime (optional, gracefully degrades) |
| Friend ordering | Drag-and-drop reorder, persisted in localStorage |
| Themes | 35 built-in DaisyUI themes, cookie-persisted |
| PWA | Installable on mobile and desktop via web app manifest |
| Profile | Username (3–20 chars) + optional display name (up to 50 chars) |
| Data export | Download all your data as JSON |
| Account deletion | Password-confirmed, cascades all data including auth user |
| Debug panel | Accessible on iOS, on error, or via localStorage flag |
| Layer | Technology |
|---|---|
| Framework | SvelteKit 2 with Svelte 5 |
| Language | TypeScript |
| Styling | Tailwind CSS v4 + DaisyUI v5 |
| Backend / DB | Supabase (PostgreSQL + Auth + Realtime) |
| Auth integration | @supabase/ssr |
| Avatars | svelte-boring-avatars (deterministic from user ID) |
| Theme switching | theme-change |
| Build tool | Vite 7 |
| Testing | Vitest |
| Linting | ESLint 9 + eslint-plugin-svelte |
| Formatting | Prettier + prettier-plugin-svelte + prettier-plugin-tailwindcss |
| CI | GitHub Actions (check, lint, test) |
- Node.js 18+ (or compatible runtime)
- A Supabase project (free tier is fine)
- npm (or pnpm / yarn)
Copy .env.example to .env.local and fill in your Supabase credentials:
cp .env.example .env.localPUBLIC_SUPABASE_URL=https://<your-project>.supabase.co
PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>Both values are safe to expose client-side (they are prefixed with PUBLIC_). The anon key is restricted by Row Level Security policies on the database.
npm installnpm run dev
# or open in browser automatically:
npm run dev -- --openNote: In development mode, TLS certificate verification is disabled (
NODE_TLS_REJECT_UNAUTHORIZED=0) to allow self-signed certificates when running Supabase locally. This is guarded byNODE_ENV === 'development'and never applies to production builds.
Other useful scripts:
npm run check # Type-check the project
npm run check:watch # Type-check in watch mode
npm run lint # Check formatting + ESLint
npm run format # Auto-format all files
npm test # Run Vitest tests
npm run test:watch # Run tests in watch modenpm run build
npm run preview # Preview the production build locallyThe project uses @sveltejs/adapter-auto, which automatically selects the right adapter for your deployment platform (Vercel, Netlify, Node, etc.). For a specific platform you may want to swap in a dedicated adapter.
- Go to supabase.com and create a new project
- Note your Project URL and anon public key from Project Settings → API
All SQL is stored in src/sql/. Run each file in order in the Supabase SQL Editor:
1. Utility functions (run first - other scripts depend on these):
src/sql/functions/update_timestamp.psql
src/sql/functions/normalize_friendship_order.psql
2. Tables:
src/sql/tables/users.psql
src/sql/tables/profiles.psql
src/sql/tables/friends.psql
src/sql/tables/friend_requests.psql
3. Trigger functions and RPC:
src/sql/functions/handle_new_user.psql
src/sql/functions/delete_user_account.psql
src/sql/functions/get_dashboard_data.psql
All scripts are idempotent (CREATE TABLE IF NOT EXISTS, CREATE OR REPLACE FUNCTION, policy existence checks), so re-running them is safe.
For live updates to work, enable Supabase Realtime replication on three tables. Either use the dashboard (Database → Replication) or run this SQL:
ALTER PUBLICATION supabase_realtime ADD TABLE friend_requests;
ALTER PUBLICATION supabase_realtime ADD TABLE friends;
ALTER PUBLICATION supabase_realtime ADD TABLE profiles;Real-time is optional - the app functions normally without it, users just need to refresh to see changes.
In your Supabase dashboard under Authentication → URL Configuration:
- Site URL: your production domain (e.g.
https://yourapp.com) - Redirect URLs: add
https://yourapp.com/auth/confirm(andhttp://localhost:5173/auth/confirmfor local dev)
The /auth/confirm endpoint handles multiple OTP types: signup confirmation, password recovery, and email change. For password resets, the recovery email links to /auth/confirm?next=/auth/reset-password, which verifies the token and redirects to the new-password form.
src/
├── app.css # Global styles
├── app.d.ts # TypeScript ambient declarations (locals types)
├── app.html # HTML shell
├── database.types.ts # Supabase-generated DB types
├── hooks.server.ts # Server hooks: Supabase client, auth guard, theme injection
│
├── lib/
│ ├── dashboard/
│ │ └── loader.ts # DashboardDataLoader class - all DB queries
│ ├── friends/
│ │ ├── api.ts # verifyFriendshipExists, checkExistingFriendRequest, checkIncomingFriendRequest
│ │ ├── order.ts # localStorage-backed friend ordering singleton
│ │ └── components/
│ │ ├── DeleteModal.svelte
│ │ ├── List.svelte # Drag-and-drop friend list with statuses
│ │ ├── ListSkeleton.svelte
│ │ ├── Requests.svelte # Send/accept/reject/cancel requests
│ │ └── RequestsSkeleton.svelte
│ ├── profile/
│ │ ├── api.ts # checkUsernameAvailability
│ │ └── validation.ts # Username/display-name constants, validators, ERROR_MESSAGES
│ ├── realtime/
│ │ └── subscriptions.ts # RealtimeSubscriptionManager class
│ ├── status/
│ │ ├── formatting.ts # formatStatusUpdatedAt, formatStatusUpdatedAtTooltip
│ │ ├── quick.ts # localStorage-backed quick status management
│ │ ├── validation.ts # MAX_STATUS_LENGTH, validateStatus
│ │ └── components/
│ │ ├── Section.svelte # Status form + quick statuses
│ │ └── Skeleton.svelte
│ └── ui/
│ ├── notifications.ts # NotificationManager, handleDatabaseError, getDisplayName
│ ├── themes.ts # List of available DaisyUI theme names
│ ├── toast.ts # Global toast notification store
│ ├── DebugPanel.svelte # Floating debug log panel (dev/iOS/error)
│ ├── Footer.svelte
│ ├── Navigation.svelte # Sticky nav with logout + settings link
│ ├── RelativeTime.svelte # Live-ticking relative timestamp component
│ ├── ThemeSelect.svelte # Theme picker component
│ ├── Toast.svelte
│ └── ToastContainer.svelte
│
├── routes/
│ ├── +layout.server.ts # Passes session + cookies to client layout
│ ├── +layout.svelte # Root layout: theme init, auth listener, toast container
│ ├── +layout.ts # Creates Supabase client (browser or server)
│ ├── +page.svelte # Landing / hero page
│ ├── auth/
│ │ ├── +layout.svelte
│ │ ├── +page.server.ts # login/signup/forgotPassword form actions
│ │ ├── +page.svelte # Login/signup tab UI with forgot password flow
│ │ ├── confirm/+server.ts # Email confirmation OTP handler (signup, recovery, email change)
│ │ ├── error/+page.svelte # Auth error page
│ │ ├── logout/+server.ts # POST endpoint: server-side sign out + cookie clear
│ │ └── reset-password/
│ │ ├── +page.server.ts # Load function (session check)
│ │ └── +page.svelte # New password form after recovery
│ └── dashboard/
│ ├── +layout.server.ts # Auth guard - redirects unauthenticated users to /
│ ├── +layout.svelte # Dashboard layout with Navigation
│ ├── +page.server.ts # Minimal load (session only)
│ ├── +page.svelte # Main dashboard page
│ └── settings/
│ ├── +page.server.ts
│ └── +page.svelte # Settings page (profile, security, quick statuses, appearance, data)
│
└── sql/
├── functions/
│ ├── delete_user_account.psql
│ ├── get_dashboard_data.psql
│ ├── handle_new_user.psql
│ ├── normalize_friendship_order.psql
│ └── update_timestamp.psql
└── tables/
├── friend_requests.psql
├── friends.psql
├── profiles.psql
└── users.psql
- Server request hits
hooks.server.ts, which creates a Supabase server client using cookies, callssafeGetSession()(validates JWT viagetUser()), and enforces the auth guard. +layout.server.tspassessessionand raw cookies to the client.+layout.tscreates a Supabase browser client (or server client during SSR) and exposessupabase,session, anduserto all child layouts/pages.- Dashboard page receives only
sessionfrom the server - all dashboard data (friends, statuses, requests) is loaded client-side viaDashboardDataLoader. This keeps server load minimal and avoids serializing large data structures through the load chain. - Real-time updates are handled by
RealtimeSubscriptionManager, which subscribes tofriend_requests,friends, andprofilestable changes. Updates are granular: profile/status changes are patched in-place from the realtime payload (zero DB calls), friend request changes trigger a targeted reload of just the request arrays, and friendship changes (rare, structural) trigger a full data reload.
auth.users (Supabase built-in)
└── public.users id, username (unique), display_name, email, created_at, updated_at
└── public.profiles id, status (max 42 chars), created_at, updated_at
public.users
└── public.friends user_id, friend_id (canonical: user_id < friend_id)
└── public.friend_requests requester_id, target_id
public.friends stores one record per friendship pair. The normalize_friendship_order trigger ensures user_id is always the lexicographically smaller UUID, enforced by a CHECK (user_id < friend_id) constraint. This prevents duplicate records and simplifies existence checks.
public.profiles stores the user's current status. Updated via upsert from the client.
Automatic provisioning: When a new auth.users row is inserted, the on_auth_user_created trigger fires handle_new_user(), which auto-generates a unique username from the email prefix and creates the corresponding users and profiles rows.
Client-side data loading for the dashboard The server only validates the session; dashboard data is fetched client-side. This simplifies the SvelteKit load chain and makes the initial page interactive faster (skeleton loaders are shown while data loads). The trade-off is that data is not available on first SSR render.
localStorage for quick statuses and friend ordering
Quick statuses and friend order are stored in the browser's localStorage rather than the database. This avoids additional schema complexity and database writes for purely presentational preferences. The trade-off is that these settings are device-specific and do not sync across browsers/devices.
Svelte 5 runes throughout
All components use Svelte 5's rune-based reactivity ($state, $derived, $effect, $props, $bindable). This is the modern Svelte 5 API and provides cleaner component logic with explicit reactivity.
Granular real-time updates Realtime events are handled with three strategies: profile/status changes are patched in-place directly from the event payload (zero database calls), friend request changes trigger a debounced (300ms) reload of just the request arrays (two small queries), and friendship changes trigger a full reload since they are rare structural events that affect the profiles subscription scope.
adapter-auto
Using @sveltejs/adapter-auto means the project deploys to any supported platform without configuration changes. Swap in a specific adapter if you need fine-grained control.
- Auth uses Supabase's email/password flow via
@supabase/ssr. - Session validation in
hooks.server.tscalls bothgetSession()andgetUser(). ThegetUser()call makes a network request to Supabase to cryptographically verify the JWT - this prevents accepting a locally-forged or replayed token. This pattern follows Supabase's security recommendations. safeGetSession()returns{ session: null, user: null }on any validation error rather than propagating the error upward.- The auth guard has two layers:
hooks.server.tsredirects unauthenticated users away from/dashboard/**to/auth, anddashboard/+layout.server.tsprovides a secondary guard that redirects to/. Authenticated users hitting exactly/authare redirected to/dashboard(sub-routes like/auth/reset-passwordare not affected). - Password reset: Users request a reset link from the login page via
resetPasswordForEmail(). The email links through/auth/confirm(OTP verification) and redirects to/auth/reset-passwordwhere they set a new password viaupdateUser(). The root layout'sonAuthStateChangelistener also handles thePASSWORD_RECOVERYevent as a safety redirect. - Password change: Authenticated users can change their password from Settings. The current password is verified via
signInWithPassword()re-authentication before callingupdateUser(). - Email change: Authenticated users can request an email change from Settings via
updateUser({ email }). Supabase sends a confirmation link to the new address; the existing/auth/confirmhandler processes theemail_changeOTP type. - Logout hits both the client-side
supabase.auth.signOut()and a server-sidePOST /auth/logoutendpoint that manually clears all Supabase auth cookies.
All four public tables have RLS enabled. The policies enforce:
| Table | SELECT | INSERT | UPDATE | DELETE |
|---|---|---|---|---|
users |
All authenticated users can read | - | Own row only | Own row only |
profiles |
All authenticated users can read | Own row only | Own row only | - |
friends |
Only if user_id or friend_id matches |
Only if user_id or friend_id matches |
- | Only if user_id or friend_id matches |
friend_requests |
Own rows (as requester or target) | Own as requester (rate-limited) | Own as target | Own as requester or target |
The read-all policy on users and profiles is intentional - users need to search for others by username and view friends' statuses.
Rate limiting: A restrictive RLS policy on friend_requests limits each user to 20 INSERT operations per hour. This is enforced at the database level via a RESTRICTIVE policy that is ANDed with the permissive INSERT policy, backed by a composite index on (requester_id, created_at).
Auth session cookies are set with:
path: '/'- applies app-widesecure: truein production (HTTPS only) - prevents cookie theft over plain HTTPsameSite: 'lax'- blocks cross-site POST requests from sending the cookie (CSRF mitigation)domainset to the request hostname in production - prevents session sharing across subdomains- Localhost/127.0.0.1:
domainis omitted to avoid browser quirks
All clients (including iOS Safari) receive sameSite: 'lax', which avoids the more restrictive none behavior that can cause issues with in-app browsers.
delete_user_account()runs asSECURITY DEFINERso it can delete fromauth.users(which the anon/authenticated role cannot do directly). It checksauth.uid()at runtime to confirm the caller is authenticated before proceeding.handle_new_user()also runs asSECURITY DEFINER SET search_path = ''to prevent search path injection attacks.- Execute permission on
delete_user_account()is granted only to theauthenticatedrole.
In development (NODE_ENV === 'development'), TLS certificate verification is disabled via NODE_TLS_REJECT_UNAUTHORIZED=0. This is logged with a visible warning. Never run production builds with this flag set.
The RealtimeSubscriptionManager class (in src/lib/realtime/subscriptions.ts) opens channels per logged-in user:
| Channel | Table | Filter |
|---|---|---|
friend_requests_from_{userId} |
friend_requests |
Server-side: requester_id=eq.{userId} |
friend_requests_to_{userId} |
friend_requests |
Server-side: target_id=eq.{userId} |
friends_user_{userId} |
friends |
Server-side: user_id=eq.{userId} |
friends_friend_{userId} |
friends |
Server-side: friend_id=eq.{userId} |
profiles_friends_{userId} |
profiles |
Server-side: id=in.(friendId1,friendId2,...) |
Each table uses two channels because Supabase Realtime only supports a single eq filter per channel, and the current user can appear in either column. The profiles channel is scoped to the user's current friend list via an in filter and is recreated whenever the friend list changes - only UPDATE events for friends' rows are delivered.
To enable Realtime, tables must be added to Supabase's publication:
ALTER PUBLICATION supabase_realtime ADD TABLE friend_requests;
ALTER PUBLICATION supabase_realtime ADD TABLE friends;
ALTER PUBLICATION supabase_realtime ADD TABLE profiles;If Realtime is not configured, the subscriptions fail silently and the app works normally with manual refresh.
| Key | Content | Purpose |
|---|---|---|
friend-order |
string[] (friend UUID array) |
Persists drag-and-drop friend order |
rez_quick_statuses |
QuickStatus[] (JSON) |
Persists user-defined quick status presets |
debug-mode |
"true" / "false" |
Controls debug panel visibility |
These are purely client-side and device-specific - they are not synced to the database. Clearing browser storage resets these to defaults.
API
- Introduce an API. View only, so people can make their own dashboards (e.g. TRMNL).