Bedrijfslogica
Pricing & Plannen
Section titled “Pricing & Plannen”Huidig: Beam is gratis. Geen billing, geen quota-enforcement op planniveau.
Gepland: Stripe integratie met plan tiers. Zie Backlog P2 #11. Openstaande vraag: per addon, per vertical pack, of flat-rate per tier (OQ-9).
Quota’s & Limieten
Section titled “Quota’s & Limieten”Beam hanteert harde limieten ongeacht plan. Bij toekomstige plannen worden deze per tier configureerbaar.
| Resource | Limiet | Enforcement |
|---|---|---|
| Media opslag per site | 500 MB | check_media_quota() DB trigger |
| Bestand upload max | 10 MB (images/docs) | API validatie |
| Video upload max | 100 MB | API validatie |
| Custom domeinen per site | 5 | API validatie |
| Sites per owner | 10 (actief) | check_site_quota() DB trigger (telt alleen deleted_at IS NULL) |
| Team uitnodiging expiry | 7 dagen | expires_at kolom |
| Pagina slug formaat | ^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$ (max 63 tekens) | Client + API validatie |
| Query resultaat limiet | 200-500 rijen | .limit() op alle Supabase queries |
| Cache purge URLs | Max 30 per request | API validatie |
Video’s en opslag
Section titled “Video’s en opslag”Video’s worden gehost bij Bunny Stream en tellen niet mee in de 500 MB site quota. Alleen images en documenten in R2 tellen mee.
Bij quota bereikt: De check_media_quota() database trigger blokkeert de INSERT. De API retourneert een foutmelding en de dashboard toont een toast: “Opslaglimiet bereikt.” De gebruiker moet bestaande media verwijderen om ruimte vrij te maken.
Rate Limiting
Section titled “Rate Limiting”| Categorie | Limiet | Scope | Response |
|---|---|---|---|
| Algemeen | 100 req/min | Per authenticated user | 429 + Retry-After |
| Uploads | 20 req/min | Per authenticated user | 429 + Retry-After |
| Ongeauthenticeerd | 30 req/min | Per IP | 429 + Retry-After |
Implementatie: In-memory sliding window per Worker isolate. Niet globaal gedeeld — voldoende voor abuse preventie. Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Rollen & Rechten
Section titled “Rollen & Rechten”Beam gebruikt een rolhierarchie per team. Elke owner kan meerdere sites beheren (max 10).
| Rol | Pagina’s | Media | Patterns | Menus | Team | Site Settings |
|---|---|---|---|---|---|---|
| Owner | CRUD | CRUD | CRUD | CRUD | Alles | Alles |
| Admin | CRUD | CRUD | CRUD | CRUD | Invite, rol wijzigen, verwijderen | Alles |
| Editor | CRUD | CRUD | CRUD | CRUD | — | — |
| Viewer | Lezen | Lezen | Lezen | Lezen | — | — |
Regels:
- Een team heeft precies 1 owner — kan niet verwijderd of gewijzigd worden
- Admins kunnen alle rollen wijzigen behalve owner (inclusief andere admins promoten/degraderen)
- Editors kunnen geen team- of site-instellingen wijzigen
- Viewers hebben alleen leesrechten
RLS & Team Membership
Section titled “RLS & Team Membership”Alle data-toegang wordt afgedwongen via Supabase RLS policies die get_user_role(auth.uid(), site_id) checken. Als een user geen team_members rij heeft voor een site, falen alle CRUD operaties.
Beschermingslagen:
- Database trigger —
add_owner_to_site_team_triggermaakt automatisch een team_members rij aan bij site creatie - Runtime safety net —
ensure_owner_team_member()SECURITY DEFINER functie, aanroepbaar viasupabase.rpc(), valideertsites.owner_id = auth.uid()voordat de rij wordt aangemaakt - Stale site detectie —
useSites()hook auto-reset naar eerste site als opgeslagen site ID niet meer in de user’s sites lijst zit - Specifieke foutmeldingen — RLS errors (42501, PGRST116) tonen “Je hebt geen toegang tot deze site” i.p.v. generiek “Opslaan mislukt”
API team endpoints gebruiken de admin Supabase client (service role) voor team_members lookups en updates, na validatie van de caller’s rechten via verifyCallerSiteRole().
Pagina Lifecycle
Section titled “Pagina Lifecycle”Aanmaken → draft → Bewerken in editor (autosave naar Zustand store) → Opslaan → PUT /pages/:id (RLS check: editor+ in team) → Publiceren → status: published → Cache purge (fire-and-forget) → Publiek zichtbaar via Astro SSRPublicatie status:
draft— Alleen zichtbaar voor teamleden in dashboardpublished— Publiek zichtbaar via de Astro site. RLS laat SELECT toe zonder auth.
Slug wijziging: Bij slug wijziging worden alle interne links op andere pagina’s automatisch bijgewerkt via POST /pages/:id/cascade-hrefs. Dit doorzoekt recursief alle blocks (nested groups, columns, hero buttons).
Team Uitnodigingen
Section titled “Team Uitnodigingen”Uitnodigen → pending (7 dagen geldig) → Accepteren → accepted (team_members record) → OF Afwijzen → declined → OF Verlopen → expired (automatisch) → OF Intrekken → revoked (door owner/admin)Validaties bij uitnodiging:
- Geen self-invite
- Geen duplicaat email (al lid of pending)
- Alleen owner/admin mag uitnodigen
- Ghost users worden opgeruimd via
delete_ghost_user()SQL functie
Media Regels
Section titled “Media Regels”Upload:
- Client berekent SHA-256 hash vóór upload
- Duplicaat check: als hash bestaat → hergebruik bestaand item
- SSRF check op externe URLs: blokkeert private IP ranges (127.x, 10.x, 172.16-31.x, 192.168.x, ::1)
Ondersteunde formaten:
| Type | Formaten | Max |
|---|---|---|
| Afbeeldingen | JPEG, PNG, SVG, WebP, GIF | 10 MB |
| Documenten | PDF, DOCX, TXT | 10 MB |
| Video’s | MP4, WebM, MOV | 100 MB |
Verwerking bij upload:
- Blur placeholder (LQIP): 20px via CF Image Resizing
- Thumbnail: 400px WebP
- Metadata opslaan (dimensions, hash, MIME type)
Cleanup: Dagelijkse cron (03:00 UTC) verwijdert orphaned media (site_id IS NULL), max 100 per run.
Custom Domeinen
Section titled “Custom Domeinen”Regels:
- Max 5 domeinen per site
- Eerste domein wordt automatisch primary
- Primary is te wijzigen via dashboard
- Bij verwijderen van primary: volgende domein wordt promoted
- Status polling: elke 5 minuten via cron
- Timeout: 48 uur zonder DNS → status: error
Geblokkeerde domeinen: Vooraf gedefinieerde lijst van domeinen die niet gekoppeld kunnen worden (bijv. builtwithbeam.com subdomains).
Site Lifecycle
Section titled “Site Lifecycle”Aanmaken → actief (deleted_at IS NULL) → Gebruiker bewerkt content, media, menus, settings → Soft delete → deleted_at = NOW() → Site verdwijnt uit dashboard en site switcher → 14 dagen grace period → hardDeleteExpiredSites() cron → definitief verwijderd → R2 bestanden opgeruimd → CF custom domains losgekoppeld → DB records verwijderd (CASCADE)Slug auto-generatie: Bij aanmaken wordt een slug automatisch gegenereerd uit de sitenaam (lowercase, spaties → hyphens, speciale tekens verwijderd). De slug is uniek per site.
Pagina slugs: Pagina slugs zijn uniek per site (UNIQUE(site_id, slug)) — dezelfde slug kan bestaan op verschillende sites.
Pattern Systeem
Section titled “Pattern Systeem”Patterns zijn herbruikbare block templates met optionele sync.
Propagatie regels:
- “Update all uses” werkt alleen op synced patterns
- Ownership check via RLS — alleen team members
- Query limiet: 500 pagina’s per propagatie
- Alle betroffenen pagina’s worden parallel bijgewerkt
- Cache purge na propagatie
Usage tracking: GET /patterns/:id/usage retourneert lijst van pagina’s die het pattern gebruiken, inclusief titel en status.