> ## Documentation Index
> Fetch the complete documentation index at: https://docs.insito.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Data model

> The objects Insito tracks — organizations, projects, surveys, respondents, responses, and how they relate.

Insito's data model is relational and multi-tenant: every row ultimately belongs
to one organization. The core tables below cover what your app and dashboard read
and write; a handful of supporting tables (variables, usage counters, screen maps,
integrations) are summarised at the end.

## Entity diagram

```mermaid theme={null}
erDiagram
  organization ||--o{ member : "has"
  organization ||--o{ project : "owns"
  project ||--o{ survey : "contains"
  project ||--o{ respondent : "knows"
  survey ||--o{ question : "has"
  survey ||--o{ impression : "records"
  survey ||--o{ response : "collects"
  respondent ||--o{ impression : "sees"
  respondent ||--o{ response : "gives"
```

Answers are **not** a separate table — they live as a JSONB array on each
`response` row.

## Core tables

### `organizations`

The billing unit. One organization = one Stripe customer, one plan, one set of
members.

| Column                   | Type | Notes                                    |
| ------------------------ | ---- | ---------------------------------------- |
| `id`                     | uuid | PK                                       |
| `name`                   | text | Workspace name                           |
| `slug`                   | text | Unique                                   |
| `plan`                   | text | `free`, `starter`, `growth`, `studio`    |
| `plan_response_limit`    | int  | Monthly cap, interpreted **per app**     |
| `plan_app_limit`         | int  | Apps included (base + purchased add-ons) |
| `stripe_customer_id`     | text | Set after first paid plan                |
| `stripe_subscription_id` | text | Set after first paid plan                |

### `members`

Links a Supabase auth user to an organization with a role.

| Column            | Type | Notes                      |
| ----------------- | ---- | -------------------------- |
| `user_id`         | uuid | FK → `auth.users.id`       |
| `organization_id` | uuid | FK                         |
| `role`            | text | `owner`, `admin`, `viewer` |

### `projects`

One app = one project. Holds the project's `api_key` (the `proj_…` secret your
SDK uses).

| Column                  | Type  | Notes                                             |
| ----------------------- | ----- | ------------------------------------------------- |
| `id`                    | uuid  | PK                                                |
| `short_id`              | text  | Short slug used in dashboard URLs                 |
| `organization_id`       | uuid  | FK                                                |
| `name`                  | text  | Display name                                      |
| `platform`              | text  | `react_native`, `ios`, `android`, `flutter`       |
| `api_key`               | text  | Project secret, `proj_<32 hex>`. Unique, indexed. |
| `auto_discover_events`  | bool  | Auto-add fired event keys to the registry         |
| `auto_discover_screens` | bool  | Auto-add visited screen paths to the registry     |
| `brand_config`          | jsonb | Brand Kit (colours, radii, branding)              |
| `labels`                | jsonb | Project-level SDK label overrides                 |
| `is_active`             | bool  | Soft-delete flag                                  |

### `surveys`

A survey definition, linked to a project.

| Column           | Type  | Notes                                                             |
| ---------------- | ----- | ----------------------------------------------------------------- |
| `id`             | uuid  | PK                                                                |
| `short_id`       | text  | Short slug used in dashboard URLs                                 |
| `project_id`     | uuid  | FK                                                                |
| `name`           | text  | Internal label (shown in the dashboard, not the modal)            |
| `status`         | text  | `draft`, `active`, `paused`, `archived`, `scheduled`, `completed` |
| `trigger_key`    | text  | Allowed chars `A–Z a–z 0–9 _`; up to 128                          |
| `throttle_days`  | int   | Minimum days between shows. Default **7**                         |
| `response_limit` | int   | Optional total-response cap (auto-pauses when hit)                |
| `response_count` | int   | Maintained by trigger                                             |
| `trigger_config` | jsonb | Trigger conditions, audience filters, frequency                   |
| `logic`          | jsonb | Branching rules between questions                                 |
| `draft`          | jsonb | Unpublished builder state                                         |

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.

| Column        | Type  | Notes                                                                           |
| ------------- | ----- | ------------------------------------------------------------------------------- |
| `id`          | uuid  | PK                                                                              |
| `survey_id`   | uuid  | FK                                                                              |
| `type`        | text  | `nps`, `rating`, `multiple_choice`, `open_text`, `welcome_screen`, `end_screen` |
| `text`        | text  | Shown to user                                                                   |
| `options`     | jsonb | Type-dependent (choices, etc.)                                                  |
| `settings`    | jsonb | Type-dependent config (scale, labels, screen copy)                              |
| `is_required` | bool  | Block "Next" until answered                                                     |
| `position`    | int   | 0-based                                                                         |

`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`.

| Column             | Type  | Notes                                                      |
| ------------------ | ----- | ---------------------------------------------------------- |
| `id`               | uuid  | PK                                                         |
| `project_id`       | uuid  | FK                                                         |
| `external_user_id` | text  | Your stable identifier (the SDK `userId`)                  |
| `platform`         | text  | From `identify()`                                          |
| `app_version`      | text  | From `identify()`                                          |
| `metadata`         | jsonb | Device metadata auto-captured by the SDK                   |
| `properties`       | jsonb | Custom business attributes from `identify({ properties })` |

### `impressions`

One row each time a survey modal is shown. Feeds the per-user show cap
(`maxShowsPerUser`) and records `shown_at`.

| Column                                       | Type        | Notes                   |
| -------------------------------------------- | ----------- | ----------------------- |
| `id`                                         | uuid        | PK                      |
| `survey_id` / `respondent_id` / `project_id` | uuid        | FKs                     |
| `shown_at`                                   | timestamptz | When the modal appeared |

### `responses`

One row per response. Counts against the app's monthly cap.

| Column                                       | Type        | Notes                               |
| -------------------------------------------- | ----------- | ----------------------------------- |
| `id`                                         | uuid        | PK                                  |
| `survey_id` / `respondent_id` / `project_id` | uuid        | FKs                                 |
| `answers`                                    | jsonb       | Array of answer objects (see below) |
| `status`                                     | text        | `partial` (autosave) or `completed` |
| `platform` / `app_version`                   | text        | Snapshot at submit time             |
| `submitted_at`                               | timestamptz | Set/updated when submitted          |

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

| Table                            | Purpose                                                               |
| -------------------------------- | --------------------------------------------------------------------- |
| `screen_maps`                    | Auto-discovered screen paths and visit counts per project             |
| `app_variables`                  | Event/screen registry with `source`, `verified`, and `approved` flags |
| `user_property_definitions`      | Schema of custom user properties for audience targeting               |
| `monthly_response_usage`         | Org-level monthly response counter                                    |
| `monthly_project_response_usage` | Per-app monthly response counter (the SDK cap)                        |
| `integrations`                   | Per-project `slack` / `webhook` / `email_digest` config               |
| `billing_events`                 | Stripe webhook audit trail (plan changes, etc.)                       |

## 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 + the `members` 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](/api/authentication) for how
API keys and JWTs authorize requests.
