Skip to content

Commit 3cadea4

Browse files
committed
move growth tracking private
1 parent 3a569bc commit 3cadea4

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

docs/supabase-data-architecture.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Supabase data architecture
2+
3+
Peels uses Supabase's Data API deliberately. Tables that are safe API surfaces stay in the exposed `public` schema with RLS and narrow grants. Implementation details and internal-only data live outside the API surface.
4+
5+
This document is safe to keep in the public repo because it describes boundaries and intent, not secrets, keys, or hidden access paths. The database policies and grants remain the source of enforcement.
6+
7+
## Schemas
8+
9+
### `public`
10+
11+
`public` contains the tables and functions the app may reach through the Supabase client. Anything in this schema should be treated as potentially API-addressable, so tables need RLS enabled and role grants should be explicit.
12+
13+
The main public tables are split into two groups:
14+
15+
- Source-of-truth tables that store real application records.
16+
- Read-model tables that expose deliberately shaped data to a specific audience.
17+
18+
### `private`
19+
20+
`private` is not an exposed Data API schema. It is for database implementation details such as trigger functions, security-definer helpers, and internal-only tables.
21+
22+
Browser clients should not query this schema. Server-side service-role code may use it for trusted internal work when needed.
23+
24+
## Source-of-truth tables
25+
26+
### `public.profiles`
27+
28+
Stores private profile state for each user, including fields that should not be broadly readable through the API. It is not a public profile directory.
29+
30+
Direct reads are owner-scoped. Public or cross-user display needs should use `profile_contact_cards`.
31+
32+
### `public.listings`
33+
34+
Stores full listing records, including owner identity and fields that should not be exposed to signed-out visitors.
35+
36+
Direct writes stay on the base table so listing create/edit/delete flows operate on the canonical record. Public browsing should use `public_listings`; authenticated listing/contact UI should use `listing_contact_cards`.
37+
38+
### `public.chat_threads`
39+
40+
Stores chat thread membership and listing context. Access is participant-scoped through RLS.
41+
42+
The app composes richer chat payloads in TypeScript instead of exposing privileged joined views.
43+
44+
### `public.chat_messages`
45+
46+
Stores chat messages. Access is participant-scoped through RLS, with trigger checks for message sender integrity and rate limits.
47+
48+
### `private.growth_tracking`
49+
50+
Stores internal growth tracking records. It is not part of the client Data API and should be read or written only by trusted server-side or database code.
51+
52+
## Read-model tables
53+
54+
### `public.public_listings`
55+
56+
Signed-out safe listing catalogue used by map, SEO, sitemap, homepage, and public listing pages.
57+
58+
This table intentionally includes public listing content such as description, accepted items, rejected items, type, area/country, coordinates, slug, and low-sensitivity presentation metadata.
59+
60+
Residential listings keep their description, accepted items, and rejected items public, but owner identity and residential media are hidden:
61+
62+
- `name` is `null` for residential rows.
63+
- `photos` is `null` for residential rows.
64+
- `avatar` is `null` for residential rows.
65+
- `owner_id`, `visibility`, raw `location`, and profile fields are not exposed.
66+
67+
Rows are maintained by database triggers from `public.listings`.
68+
69+
### `public.listing_contact_cards`
70+
71+
Authenticated listing/contact read model for visible listings, own listings, and chat participant flows.
72+
73+
It includes listing data plus the owner id and safe owner display fields needed to start or display chats. It is still protected by RLS; it is not a signed-out API.
74+
75+
Rows are maintained by database triggers from `public.listings` and `public.profiles`.
76+
77+
### `public.profile_contact_cards`
78+
79+
Authenticated profile display read model with only:
80+
81+
- `id`
82+
- `first_name`
83+
- `avatar`
84+
85+
It is readable for the user themself and for chat participants who need display data. It is not a full profile export.
86+
87+
Rows are maintained by database triggers from `public.profiles`.
88+
89+
## Database helpers
90+
91+
Security-definer helper functions live in `private`, not `public`. Public security-definer functions and public definer views should be avoided because they create confusing API surfaces and Supabase dashboard warnings.
92+
93+
The current helper functions refresh read-model rows, enforce chat message/thread integrity, and support auth/profile setup.
94+
95+
## Change rules
96+
97+
- Add forward migrations only; do not edit historical migrations once they may have been applied to a preview or production branch.
98+
- Keep `public` tables RLS-enabled.
99+
- Do not add public security-definer views or functions.
100+
- Prefer read-model tables for deliberate API shapes when column-level privacy matters.
101+
- Prefer TypeScript composition for authenticated interaction state such as chat, where materialised joined database views make privacy harder to reason about.
102+
- Keep internal-only tables in `private` unless there is a clear client API reason not to.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- growth_tracking is internal analytics state, not a Data API surface.
2+
-- Move the existing table and identity sequence without rewriting or deleting rows.
3+
4+
alter table if exists public.growth_tracking set schema private;
5+
alter sequence if exists public.growth_tracking_id_seq set schema private;
6+
7+
revoke all privileges on table private.growth_tracking from anon, authenticated, public;
8+
revoke all privileges on sequence private.growth_tracking_id_seq from anon, authenticated, public;
9+
10+
grant all privileges on table private.growth_tracking to service_role;
11+
grant all privileges on sequence private.growth_tracking_id_seq to service_role;

0 commit comments

Comments
 (0)