Supabase — Validation, Multi-Tenant & Storage Standard
Three validation layers, frontend error handling contract, multi-tenant workspace scoping, role permission matrix, storage bucket security.
24. Validation Standards
Backend Must Validate
| Field Type | Validation |
|---|---|
| UUID | Valid UUID format and accessible by user. |
| Trim, lowercase, valid format. | |
| Phone | E.164 format. |
| Text | Trim, min/max length. |
| Number | Min/max/decimal precision. |
| Enum | Must be allowed value. |
| URL | Must be valid http/https. |
| Date | Valid ISO date and logical range. |
| File | MIME, extension, size, ownership. |
| JSON | Schema validation. |
Validation Must Exist In Three Layers
| Layer | Purpose |
|---|---|
| Frontend | Good UX. |
| Edge Function/RPC | Security and business rules. |
| Database | Final protection through constraints/RLS. |
25. Frontend Error Handling Contract
Backend developer must tell frontend exactly what to do.
| Backend Error | Frontend Action |
|---|---|
UNAUTHENTICATED |
Redirect to login/session expired modal. |
FORBIDDEN |
Show no-permission message or hide action. |
VALIDATION_ERROR |
Highlight field-level errors. |
NOT_FOUND |
Show 404/resource unavailable. |
CONFLICT |
Show duplicate/conflict message. |
RATE_LIMITED |
Disable retry temporarily, show cooldown. |
INTERNAL_ERROR |
Show generic retry message. |
EXTERNAL_SERVICE_ERROR |
Show integration-specific failure. |
26. API Documentation Must Include Field-Level Errors
Example:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Please fix the highlighted fields.",
"fields": {
"name": "Project name is required.",
"workspace_id": "Workspace ID must be a valid UUID."
}
}
}
Frontend should not parse raw database messages directly. Backend should map them.
27. Multi-Tenant SaaS Standards
If the app has multiple companies, clients, hotels, agencies, teams, or workspaces, every important table must include tenant scoping.
Required Pattern
workspace_id / organization_id must exist on tenant-owned records.
Every Query Must Be Scoped
Bad:
supabase.from("projects").select("*");
Good:
supabase
.from("projects")
.select("*")
.eq("workspace_id", workspaceId);
But still do not trust frontend workspaceId. RLS must verify user membership.
28. Workspace Membership Table Standard
create table public.workspace_members (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
role text not null check (role in ('owner', 'admin', 'manager', 'member', 'viewer')),
created_at timestamptz not null default now(),
unique (workspace_id, user_id)
);
This table becomes the base of most RLS policies.
29. Role Permission Matrix Documentation
Backend must document permissions like this:
| Action | Owner | Admin | Manager | Member | Viewer |
|---|---|---|---|---|---|
| View projects | Yes | Yes | Yes | Yes | Yes |
| Create project | Yes | Yes | Yes | No | No |
| Update project | Yes | Yes | Yes | Own only | No |
| Delete project | Yes | No | No | No | No |
| Invite member | Yes | Yes | No | No | No |
| Change billing | Yes | No | No | No | No |
Frontend uses this to hide/show actions, but backend/RLS remains the real authority.
30. Storage Security Standards
For Supabase Storage:
| Rule | Standard |
|---|---|
| Buckets | Separate public/private buckets. |
| Public Files | Only for assets safe to expose. |
| Private Files | Use signed URLs. |
| Upload Policy | User can upload only to allowed path. |
| File Path | Include tenant/user scope. |
| File Size | Validate. |
| MIME Type | Validate. |
| Delete Permission | Owner/admin only. |
Path Standard
workspace/{workspace_id}/projects/{project_id}/files/{file_id}.pdf
Never allow users to upload to arbitrary paths.
31. Storage RLS Policy Pattern
Example:
create policy "Workspace members can view workspace files"
on storage.objects
for select
to authenticated
using (
bucket_id = 'project-files'
and exists (
select 1
from public.workspace_members wm
where wm.user_id = auth.uid()
and storage.foldername(name)[1] = 'workspace'
and wm.workspace_id::text = storage.foldername(name)[2]
)
);