Entity diagram
Answers are not a separate table — they live as a JSONB array on eachresponse row.
Core tables
organizations
The billing unit. One organization = one Stripe customer, one plan, one set of
members.
members
Links a Supabase auth user to an organization with a role.
projects
One app = one project. Holds the project’s api_key (the proj_… secret your
SDK uses).
surveys
A survey definition, linked to a project.
Scheduling and behaviour toggles (
show_branding, show_question_numbers,
allow_back, autosave_progress, schedule window) are added by later migrations.
questions
Belongs to a survey. position is the order in the modal.
welcome_screen and end_screen are steps in the flow, not answerable questions.
respondents
Created on first identify() per (project, external_user_id). Multiple devices
share one respondent if you pass the same userId.
impressions
One row each time a survey modal is shown. Feeds the per-user show cap
(maxShowsPerUser) and records shown_at.
responses
One row per response. Counts against the app’s monthly cap.
Each element of
answers is { questionId, type, value }, where value is a
scalar or array (not a wrapper object):
nps:value: 9(number 0–10)rating:value: 4(number)multiple_choice:value: ["option_a", "option_b"](array of strings)open_text:value: "free text"(string)
Supporting tables
Deletes and history
- Projects are soft-deleted (
is_active = false). - Surveys can be hard-deleted from the dashboard, which removes their
responses. Use
status = "paused"to deactivate a survey without deleting it. - Respondents and responses are effectively immutable once written (partial responses are updated in place until completed).
RLS scope
Every table has Postgres Row Level Security. Dashboard access is scoped by Supabase auth + themembers table (get_user_org_ids()), so members only see
rows in their own organization. The SDK uses a project API key that binds every
request to a single project. See Authentication for how
API keys and JWTs authorize requests.