Files
myeasycms-v2/docs/notifications/notifications-schema.mdoc
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

247 lines
6.6 KiB
Plaintext

---
status: "published"
title: "Notifications Database Schema"
label: "Database Schema"
description: "Understand the notifications table structure, Row Level Security policies, and how to extend the schema."
order: 3
---
The notifications system uses a single table with Row Level Security. This page documents the schema, security policies, and extension patterns.
## Table structure
```sql
create table if not exists public.notifications (
id bigint generated always as identity primary key,
account_id uuid not null references public.accounts(id) on delete cascade,
type public.notification_type not null default 'info',
body varchar(5000) not null,
link varchar(255),
channel public.notification_channel not null default 'in_app',
dismissed boolean not null default false,
expires_at timestamptz default (now() + interval '1 month'),
created_at timestamptz not null default now()
);
```
### Columns
| Column | Type | Default | Description |
|--------|------|---------|-------------|
| `id` | `bigint` | Auto-generated | Primary key |
| `account_id` | `uuid` | Required | Personal or team account ID |
| `type` | `notification_type` | `'info'` | Severity: info, warning, error |
| `body` | `varchar(5000)` | Required | Message text or translation key |
| `link` | `varchar(255)` | `null` | Optional URL for clickable notifications |
| `channel` | `notification_channel` | `'in_app'` | Delivery method |
| `dismissed` | `boolean` | `false` | Has user dismissed this notification |
| `expires_at` | `timestamptz` | Now + 1 month | Auto-expiration timestamp |
| `created_at` | `timestamptz` | Now | Creation timestamp |
### Enums
```sql
create type public.notification_type as enum('info', 'warning', 'error');
create type public.notification_channel as enum('in_app', 'email');
```
## Row Level Security
Three key security constraints:
### 1. Read policy
Users can read notifications for their personal account or any team account they belong to:
```sql
create policy notifications_read_self on public.notifications
for select to authenticated using (
account_id = (select auth.uid())
or has_role_on_account(account_id)
);
```
### 2. Update policy
Users can update notifications they have access to:
```sql
create policy notifications_update_self on public.notifications
for update to authenticated using (
account_id = (select auth.uid())
or has_role_on_account(account_id)
);
```
### 3. Update trigger (dismissed only)
A trigger prevents updating any field except `dismissed`:
```sql
create or replace function kit.update_notification_dismissed_status()
returns trigger set search_path to '' as $$
begin
old.dismissed := new.dismissed;
if (new is distinct from old) then
raise exception 'UPDATE of columns other than "dismissed" is forbidden';
end if;
return old;
end;
$$ language plpgsql;
create trigger update_notification_dismissed_status
before update on public.notifications
for each row execute procedure kit.update_notification_dismissed_status();
```
This ensures users cannot modify notification content, type, or other fields after creation.
### Insert permissions
Only `service_role` can insert notifications:
```sql
revoke all on public.notifications from authenticated, service_role;
grant select, update on table public.notifications to authenticated, service_role;
grant insert on table public.notifications to service_role;
```
This is why you must use `getSupabaseServerAdminClient()` when creating notifications.
## Indexes
One composite index optimizes the common query pattern:
```sql
create index idx_notifications_account_dismissed
on notifications (account_id, dismissed, expires_at);
```
This index supports queries that filter by account, dismissed status, and expiration.
## Realtime
The table is added to Supabase Realtime publication:
```sql
alter publication supabase_realtime add table public.notifications;
```
This enables the real-time subscription feature when `NEXT_PUBLIC_REALTIME_NOTIFICATIONS=true`.
## Common modifications
### Adding a read_at timestamp
Track when notifications were first viewed (not just dismissed):
```sql
alter table public.notifications
add column read_at timestamptz default null;
```
Update the trigger to allow `read_at` updates:
```sql
create or replace function kit.update_notification_status()
returns trigger set search_path to '' as $$
begin
old.dismissed := new.dismissed;
old.read_at := new.read_at;
if (new is distinct from old) then
raise exception 'UPDATE of columns other than "dismissed" and "read_at" is forbidden';
end if;
return old;
end;
$$ language plpgsql;
```
### Adding notification categories
Extend with a category for filtering:
```sql
create type public.notification_category as enum(
'system',
'billing',
'team',
'content'
);
alter table public.notifications
add column category public.notification_category default 'system';
```
### Batch deletion of old notifications
Create a function to clean up expired notifications:
```sql
create or replace function kit.cleanup_expired_notifications()
returns integer
language plpgsql
security definer
set search_path to ''
as $$
declare
deleted_count integer;
begin
with deleted as (
delete from public.notifications
where expires_at < now()
returning id
)
select count(*) into deleted_count from deleted;
return deleted_count;
end;
$$;
```
Call this from a cron job, Supabase Edge Function, or pg_cron extension.
## Testing RLS policies
The schema includes pgTAP tests in `apps/web/supabase/tests/database/notifications.test.sql`:
```sql
-- Users cannot insert notifications
select throws_ok(
$$ insert into notifications (account_id, body) values ('...', 'test') $$,
'new row violates row-level security policy for table "notifications"'
);
-- Service role can insert
set role service_role;
select lives_ok(
$$ insert into notifications (account_id, body) values ('...', 'test') $$
);
```
Run tests with:
```bash
pnpm --filter web supabase test db
```
Expected output on success:
```
# Running: notifications.test.sql
ok 1 - Users cannot insert notifications
ok 2 - Service role can insert notifications
ok 3 - Users can read their own notifications
...
```
## Related documentation
- [Notifications overview](/docs/next-supabase-turbo/notifications): Feature overview and when to use notifications
- [Configuration](/docs/next-supabase-turbo/notifications/notifications-configuration): Environment variables and feature flags
- [Sending notifications](/docs/next-supabase-turbo/notifications/sending-notifications): Server-side API for creating notifications
- [UI Components](/docs/next-supabase-turbo/notifications/notifications-components): Display notifications in your app