Manizales de Pie has three roles stored in theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/astrxnomo/manizalesdepie/llms.txt
Use this file to discover all available pages before exploring further.
profile.role column. Roles control which destructive actions — hiding, deleting, and closing work orders — are available on any given session. Authorization is enforced inside the DAL policy predicates and never assumed from the request origin: a server action is a public POST endpoint, and arriving through the app’s own form is not a fact the server gets to take for granted.
Role table
| Role | Spanish | What they can do |
|---|---|---|
visitor | Visitante | Read all published content; post anonymous reports, confirmations, and work order thread updates |
contributor | Colaborador | Same as visitor; can also create volunteer calls (requires a Google account) |
curator | Curador | All of the above, plus hide/show any entity, close work orders, move pins anywhere in the city, and access the admin actions strip on every card |
contributor (the handle_new_user trigger default). The visitor role is the safe fallback for a session that has a valid JWT but whose profile row does not exist yet — returning null in that state was the bug that kept showing “Entrar” to someone who had just come back from Google.
Admin actions strip
WhenisAdmin is true (from WorkspaceContext), every entity card renders an additional strip below its content. The AdminActions component (app/(map)/_components/admin-actions.tsx) is shared across all entity types — sites, work orders, animals, resource offers — so a curator learns the controls once:
Hide / Publish
Flips the
published boolean. This is reversible — the row stays in the database, it simply stops appearing in public queries. A hidden row is shown to the curator with an amber strip so it is distinguishable while scrolling the map.Delete
Two-step inline confirmation (“¿Seguro? Se borra para siempre” → “Sí, borrar”). This is a real
DELETE FROM — for spam and test rows only. Content that has simply gone stale should be hidden, not deleted.ADMIN_LABEL in lib/labels.ts. The vocabulary — hide, publish, delete — is identical across all entity types by design.
Policy predicates
All authorization is implemented as pure functions in<module>.policy.ts files under data/. A policy function takes only the data it needs (typically a CurrentUser | null) and returns a boolean. It never touches the database, reads from the session, or produces side effects — which is what makes the rules testable without a database and readable as a plain list of what the product permits.
Examples from the codebase:
Assigning the curator role
There is no UI for role assignment. It is done directly in the database:20260814010000_bootstrap_curator.sql migration shows the pattern for the first curator: it recreates the handle_new_user function (via CREATE OR REPLACE) to assign curator to a specific email address at account-creation time. The address is in the repo by design — it grants nothing to whoever reads it, because the account still has to pass Google OAuth. What it does buy is an auditable answer to “who made themselves curator, and when.”
Open publication model
Items submitted through public forms go live immediately (published = true by default, set by migration 20260814020000_open_publication.sql). This follows Ushahidi’s documented guidance for fast emergencies: holding reports until a curator approves them makes the reviewer the bottleneck, and information that arrives late to a field team is the same as information that never arrived.
The seed data (supabase/seed.sql) uses published = false because its coordinates are approximate and unverified. Sending someone to the wrong shelter is worse than having no pin at all. Curators publish seed rows after geocoding them.
A curator’s primary moderation action is therefore toggling published off to hide spam or unverified content, not approving an incoming queue. The record stays in the database; only its visibility changes.
