Data Model
Version: 1.0.0
This document describes the database schema for notifito, including entity relationships, table structures, and indexes.
Entity Relationship Diagram
Tenancy Tables
tenants
Seller organization. Every other table carries a tenant_id.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
name |
string | NOT NULL | Display name |
slug |
string | UNIQUE, NOT NULL | URL-friendly identifier |
timezone |
string | DEFAULT 'UTC' | Seller's timezone |
domains |
jsonb | DEFAULT '[]' | Registered domains for widget CORS |
from_name |
string | NOT NULL | Email sender display name |
from_email |
string | NOT NULL | Email sender address |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- tenants_slug_unique — UNIQUE on slug
users
Seller staff members, scoped to a tenant.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
name |
string | NOT NULL | Display name |
email |
string | NOT NULL | Email address |
password |
string | NOT NULL | Hashed password |
email_verified_at |
timestamp | NULLABLE | Email verification timestamp |
remember_token |
string | NULLABLE | Remember me token |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- users_tenant_id_email_unique — UNIQUE on (tenant_id, email)
api_keys
Authentication keys for widget and push API.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
name |
string | NOT NULL | Key name (e.g., "Widget key") |
kind |
varchar(16) | NOT NULL | publishable or secret |
prefix |
varchar(16) | UNIQUE, NOT NULL | First 16 chars for lookup |
hash |
char(64) | NOT NULL | SHA-256 hash of full key |
reusable_plaintext |
varchar(64) | NULLABLE | Plaintext for dashboard key only |
last_used_at |
timestamp | NULLABLE | Last usage timestamp |
revoked_at |
timestamp | NULLABLE | Revocation timestamp |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- api_keys_prefix_unique — UNIQUE on prefix
- api_keys_tenant_id_kind_index — on (tenant_id, kind)
Key Format:
- Publishable: pk_live_<24 random chars> (total 32 chars)
- Secret: sk_live_<24 random chars> (total 32 chars)
- Prefix: First 16 chars used for indexed lookup
- Hash: SHA-256 of full plaintext, compared in constant time
Catalog Tables
item_types
Product/service types defined by sellers.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
name |
string | NOT NULL | Display name |
slug |
string | NOT NULL | URL-friendly identifier |
attribute_schema |
jsonb | NOT NULL | Ordered list of attribute definitions |
schema_version |
integer | DEFAULT 1 | Current schema version |
notify_policy |
varchar(32) | DEFAULT 'all' | all or capacity_multiple |
notify_factor |
integer | DEFAULT 3 | Multiplier for capacity policy |
subscription_mode |
varchar(32) | DEFAULT 'once' | once or recurring |
action_url_template |
string | NULLABLE | URL template for call-to-action |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- item_types_tenant_id_slug_unique — UNIQUE on (tenant_id, slug)
attribute_schema Structure:
[
{
"key": "size",
"label": "Size",
"type": "enum",
"options": ["40", "41", "42"],
"required": true,
"matchable": true,
"multiple": false
}
]
items
Named instances of item types (optional).
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
item_type_id |
uuid | FK → item_types, CASCADE DELETE | Parent type |
name |
string | NOT NULL | Display name |
external_ref |
string | NULLABLE | External reference (e.g., SKU) |
action_url |
string | NULLABLE | Direct link to item |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- items_item_type_id_external_ref_unique — UNIQUE on (item_type_id, external_ref) WHERE external_ref IS NOT NULL
item_type_schema_versions
Archive of previous schema versions.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
item_type_id |
uuid | FK → item_types, CASCADE DELETE | Parent type |
version |
integer | NOT NULL | Version number |
definitions |
jsonb | NOT NULL | Schema at this version |
created_at |
timestamp | NOT NULL | Creation timestamp |
Indexes:
- item_type_schema_versions_item_type_id_version_unique — UNIQUE on (item_type_id, version)
People and Consent Tables
consumers
End users waiting for notifications, scoped to a tenant.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
contact_points
Email/phone/WhatsApp addresses for consumers.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
consumer_id |
uuid | FK → consumers, CASCADE DELETE | Owning consumer |
channel |
varchar(16) | NOT NULL | email, sms, whatsapp, voice |
value |
string | NOT NULL | Normalized contact value |
verified_at |
timestamp | NULLABLE | Verification timestamp |
unsubscribed_at |
timestamp | NULLABLE | Unsubscription timestamp |
invalidated_at |
timestamp | NULLABLE | Invalidation timestamp (hard bounce) |
invalidation_detail |
string | NULLABLE | Reason for invalidation |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- contact_points_tenant_id_channel_value_unique — UNIQUE on (tenant_id, channel, value)
Sendable Condition:
- verified_at IS NOT NULL
- unsubscribed_at IS NULL
- invalidated_at IS NULL
consent_events
Append-only audit trail for consent changes.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
contact_point_id |
uuid | FK → contact_points, CASCADE DELETE | Related contact point |
subscription_id |
uuid | NULLABLE | Related subscription |
action |
varchar(16) | NOT NULL | granted or withdrawn |
source |
varchar(32) | NOT NULL | widget, confirmation_link, unsubscribe_link, dashboard |
ip_address |
string | NULLABLE | Client IP address |
user_agent |
string | NULLABLE | Client user agent |
created_at |
timestamp | NOT NULL | Event timestamp |
Constraints: - Append-only: No UPDATE or DELETE operations allowed (enforced by database trigger)
Subscription Tables
subscriptions
Active waiting subscriptions.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
consumer_id |
uuid | FK → consumers, CASCADE DELETE | Owning consumer |
contact_point_id |
uuid | FK → contact_points | Contact point |
item_type_id |
uuid | FK → item_types | Target item type |
item_id |
uuid | FK → items, NULLABLE | Target item (optional) |
attributes |
jsonb | NOT NULL | Subscription attributes |
schema_version |
integer | NOT NULL | Schema version at capture |
channels |
jsonb | DEFAULT '["email"]' | Notification channels |
fingerprint |
char(64) | NOT NULL | Deduplication hash |
status |
varchar(32) | NOT NULL | Subscription status |
last_notified_at |
timestamp | NULLABLE | Last notification timestamp |
cooldown_until |
timestamp | NULLABLE | Cooldown expiry |
expires_at |
timestamp | NOT NULL | Subscription expiry (90 days) |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Indexes:
- subscriptions_tenant_id_fingerprint_unique — UNIQUE on (tenant_id, fingerprint)
- subscriptions_tenant_id_item_type_id_status_index — on (tenant_id, item_type_id, status)
- subscriptions_attributes_gin — GIN on attributes
Status Flow:
pending_confirmation → active → notified
→ expired
→ unsubscribed
Fingerprint: SHA-256 hash of normalized attributes, used to prevent duplicate subscriptions.
subscription_tokens
Confirmation and unsubscribe tokens.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
subscription_id |
uuid | FK → subscriptions, CASCADE DELETE | Related subscription |
contact_point_id |
uuid | FK → contact_points | Related contact point |
purpose |
varchar(16) | NOT NULL | confirm or unsubscribe |
token_hash |
char(64) | NOT NULL | SHA-256 hash of plaintext |
reusable_plaintext |
varchar(64) | NULLABLE | Plaintext for reusable tokens |
expires_at |
timestamp | NULLABLE | Token expiry (72h for confirm) |
used_at |
timestamp | NULLABLE | Usage timestamp |
created_at |
timestamp | NOT NULL | Creation timestamp |
Token Rules: - Confirm: Single-use, expires after 72 hours - Unsubscribe: Reusable, never expires, stored in plaintext
Event Tables
availability_events
Seller availability signals.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
item_type_id |
uuid | FK → item_types | Target item type |
item_id |
uuid | FK → items, NULLABLE | Target item (optional) |
attributes |
jsonb | NOT NULL | Event attributes |
capacity |
integer | NULLABLE | Available capacity |
available_from |
timestamp | NULLABLE | Availability window start |
available_until |
timestamp | NULLABLE | Availability window end |
source |
varchar(32) | NOT NULL | push_api, dashboard, scraper, schedule |
idempotency_key |
string | UNIQUE, NOT NULL | Deduplication key |
correlation_id |
string | NOT NULL | Request correlation ID |
occurred_at |
timestamp | NOT NULL | Event timestamp |
created_at |
timestamp | NOT NULL | Creation timestamp |
Indexes:
- availability_events_idempotency_key_unique — UNIQUE on idempotency_key
notifications
Sent notification records.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | FK → tenants, CASCADE DELETE | Owning tenant |
subscription_id |
uuid | FK → subscriptions | Related subscription |
availability_event_id |
uuid | FK → availability_events | Triggering event |
contact_point_id |
uuid | FK → contact_points | Target contact point |
delivery_id |
uuid | NULLABLE | Related delivery |
status |
varchar(32) | NOT NULL | sent, delivered, failed |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Constraints:
- notifications_subscription_id_availability_event_id_unique — UNIQUE on (subscription_id, availability_event_id)
outbox_messages
Transactional outbox for reliable event publishing.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
topic |
string | NOT NULL | Event topic |
payload |
jsonb | NOT NULL | Event payload |
correlation_id |
string | NOT NULL | Request correlation ID |
published_at |
timestamp | NULLABLE | Publication timestamp |
error |
string | NULLABLE | Last error message |
created_at |
timestamp | NOT NULL | Creation timestamp |
Indexes:
- outbox_messages_published_at_index — on published_at WHERE published_at IS NULL
Dispatch Tables (apps/dispatch database)
deliveries
Delivery state tracking.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid | PK | Unique identifier |
tenant_id |
uuid | NOT NULL | Owning tenant |
subscription_id |
uuid | NOT NULL | Related subscription |
contact_point_id |
uuid | NOT NULL | Target contact point |
channel |
varchar(16) | NOT NULL | Delivery channel |
status |
varchar(32) | NOT NULL | Delivery status |
provider_message_id |
string | NULLABLE | Provider's message ID |
dedupe_key |
string | UNIQUE, NOT NULL | Deduplication key |
error |
string | NULLABLE | Error message |
attempt |
integer | DEFAULT 1 | Retry attempt count |
queued_at |
timestamp | NOT NULL | Queue timestamp |
sending_at |
timestamp | NULLABLE | Send start timestamp |
accepted_at |
timestamp | NULLABLE | Provider acceptance timestamp |
delivered_at |
timestamp | NULLABLE | Delivery confirmation timestamp |
failed_at |
timestamp | NULLABLE | Failure timestamp |
bounced_at |
timestamp | NULLABLE | Bounce timestamp |
created_at |
timestamp | NOT NULL | Creation timestamp |
updated_at |
timestamp | NOT NULL | Last update timestamp |
Status Flow:
queued → sending → accepted → delivered
→ failed
→ bounced
Related Documentation
- Architecture Overview — System design and decisions
- Communication — Event bus and queues
- Attribute Types — The seven data types