A role-by-role audit of who can do what across the backend API, the admin frontend, Better Auth, the photo API and the deployment config. It records the effective permission matrix for admin, organizer, staff and marketing, where the frontend drifts from what the API actually allows, and a prioritised remediation plan. Committed as a reference for that remediation work; the findings in section 11 describe the current state, not fixes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
37 KiB
Spanglish Staff Permissions Report
Audit date: 22 August 2026
Repository: /home/dev/Spanglish
Scope: Backend API, frontend admin interface, authentication, database schema and migrations, photo API, tests, deployment configuration, and documentation.
Executive summary
Spanglish currently uses four staff roles:
adminorganizerstaffmarketing
The platform also has the non-staff user role.
These values are flat role strings, not a true rank hierarchy. There is no coded inheritance such as “admin automatically inherits organizer.” Each protected API endpoint explicitly lists every role it permits. Consequently, adding a new role or changing a role’s intended scope requires reviewing every relevant endpoint.
The effective model is:
- Admin: full platform administration.
- Organizer: broad event, attendee, payment, email, media, and gallery operations, without user administration or global settings.
- Staff: door, scanner, check-in, walk-in, and selected ticket operations.
- Marketing: newsletter subscriber access and draft-event visibility, although its frontend is currently misaligned with its API permissions.
- User: self-service customer functions only.
The backend API is the authoritative permission boundary. Nginx does not enforce roles, and Next.js middleware only checks whether a session cookie exists. Frontend navigation and redirects improve the user experience but do not provide security.
Several important mismatches currently exist:
- Marketing is directed to the Contacts page, but its API denies marketing users.
- Staff can navigate to event detail pages whose ticket and email requests deny staff.
- Staff can see event CRUD controls that the API rejects.
- Organizer can access Payments and Payment Options pages that call admin-only APIs.
/admin/ticketsis linked from the dashboard but missing from the sidebar and route permission map.
The most significant authorization weakness found is:
POST /api/tickets/:id/mark-payment-sentdoes not require authentication or ticket ownership. Anyone who knows a ticket UUID can move the payment into review.
1. Canonical role model
1.1 Defined roles
| Role | Staff role | Default | Intended purpose |
|---|---|---|---|
admin |
Yes | First registered account only | Full platform administration |
organizer |
Yes | No | Events and operational management |
staff |
Yes | No | Door, scanner, and check-in operations |
marketing |
Yes | No | Subscriber and marketing operations |
user |
No | Yes | Customer self-service |
The SQLite application schema defines:
role: text('role', {
enum: ['admin', 'organizer', 'staff', 'marketing', 'user'],
})
.notNull()
.default('user')
Source: backend/src/db/schema.ts:14
The same list is validated by the user update API:
role: z.enum(['admin', 'organizer', 'staff', 'marketing', 'user']).optional()
Source: backend/src/routes/users.ts:23
The frontend mirrors it in its API types:
role: 'admin' | 'organizer' | 'staff' | 'marketing' | 'user'
Source: frontend/src/lib/api/types.ts:187
1.2 No role inheritance
The authorization middleware checks exact role membership:
if (roles && !roles.includes(user.role)) {
return c.json({ error: 'Forbidden' }, 403);
}
Source: backend/src/lib/auth.ts:76-90
For example:
requireAuth(['admin', 'organizer', 'staff'])
permits those three roles only. It does not imply a hierarchy, and it does not include marketing unless marketing is explicitly listed.
The codebase uses several repeated role groups:
| Group or convention | Roles |
|---|---|
| Better Auth administrators | admin |
| Application “admin” flag | admin, organizer |
| Admin-interface access | admin, organizer, staff, marketing |
| Door staff | admin, organizer, staff |
| Privileged draft-event viewers | admin, organizer, staff, marketing |
| Photo API administrators | admin, organizer |
1.3 Account-status restrictions
A role alone is not sufficient. Backend session resolution rejects:
- banned users;
- users whose
accountStatusis notactive; - requests without a valid Better Auth session.
Source: backend/src/lib/auth.ts:38-73
This means suspended accounts immediately lose API access even if they retain a staff role.
2. Authorization layers
2.1 Nginx
Production nginx proxies:
/api/photos/to the Go photo API;/apito the Node backend;/uploadsto the backend;- all other paths to the Next.js frontend.
It does not validate sessions or roles.
Source: deploy/prod_nginx/frontend.nginx.conf:46-114
2.2 Next.js middleware
Next.js middleware checks only for the presence of a session cookie on /admin/* and /dashboard/*.
It does not:
- validate the cookie;
- load the user;
- verify the account status;
- verify a role;
- stop a regular signed-in user from receiving the admin application shell.
Source: frontend/src/middleware.ts:27-44
This is an initial navigation convenience, not an authorization boundary.
2.3 Frontend authentication context
The frontend computes:
const isAdmin =
user?.role === 'admin' ||
user?.role === 'organizer';
const hasAdminAccess =
user?.role === 'admin' ||
user?.role === 'organizer' ||
user?.role === 'staff' ||
user?.role === 'marketing';
Source: frontend/src/context/AuthContext.tsx:181-182
hasAdminAccess controls whether staff-oriented users receive an Admin link and can remain in the admin layout. isAdmin is currently exported but not used by frontend pages.
2.4 Admin layout
The admin layout:
- filters navigation items by role;
- redirects known disallowed routes;
- sends staff to
/admin/scanner; - sends marketing to
/admin/contacts; - sends admin and organizer to
/admin.
Source: frontend/src/app/admin/layout.tsx:63-114
These are client-side controls. Direct API calls remain governed by the backend.
2.5 Backend API
The backend is authoritative. It uses:
requireAuth()for any active authenticated user;requireAuth([...roles])for exact role allowlists;- inline role checks;
- resource ownership checks;
- ticket or booking UUIDs as capability tokens in selected public flows.
Source: backend/src/lib/auth.ts
2.6 Photo API
The Go photo API independently treats:
admin;organizer;
as photo administrators.
Staff and marketing do not receive photo administration access.
Source: photo-api/internal/auth/auth.go:31-48
3. Effective permission matrix
“Allowed” below means the backend API permits the role. A visible page or button does not change this result.
| Capability | Admin | Organizer | Staff | Marketing |
|---|---|---|---|---|
| Enter the staff interface | Allowed | Allowed | Allowed | Allowed |
| Admin dashboard | Allowed | Allowed | Denied | Denied |
| Analytics | Allowed | Denied | Denied | Denied |
| List all users | Allowed | Denied | Denied | Denied |
| Assign staff roles | Allowed | Denied | Denied | Denied |
| Suspend users | Allowed | Denied | Denied | Denied |
| Delete users | Allowed | Denied | Denied | Denied |
| View published events | Allowed | Allowed | Allowed | Allowed |
| View draft events | Allowed | Allowed | Allowed | Allowed |
| Create events | Allowed | Allowed | Denied | Denied |
| Update events | Allowed | Allowed | Denied | Denied |
| Duplicate events | Allowed | Allowed | Denied | Denied |
| Delete events | Allowed | Denied | Denied | Denied |
| View event attendees | Allowed | Allowed | Allowed | Denied |
| Door attendee search | Allowed | Allowed | Allowed | Denied |
| Check in attendees | Allowed | Allowed | Allowed | Denied |
| Undo check-in | Allowed | Allowed | Allowed | Denied |
| Create walk-ins | Allowed | Allowed | Allowed | Denied |
| View door summary | Allowed | Allowed | Allowed | Denied |
| List all tickets | Allowed | Allowed | Denied | Denied |
| Validate/search tickets | Allowed | Allowed | Allowed | Denied |
| Add ticket notes | Allowed | Allowed | Allowed | Denied |
| Mark cash payment paid | Allowed | Allowed | Allowed | Denied |
| Create tickets administratively | Allowed | Allowed | Allowed | Denied |
| Cancel another user’s ticket | Allowed | Denied | Denied | Denied |
| View payment administration | Allowed | Partial | Denied | Denied |
| Approve/reject payments | Allowed | Allowed | Denied | Denied |
| Refund payments | Allowed | Denied | Denied | Denied |
| Global payment options | Allowed | Denied | Denied | Denied |
| Event payment overrides | Allowed | Allowed | Denied | Denied |
| Contacts inbox | Allowed | Allowed | Denied | Denied |
| Newsletter subscriber list | Allowed | Denied | Denied | Allowed |
| Read/send operational email | Allowed | Allowed | Denied | Denied |
| Create/update/delete email templates | Allowed | Denied | Denied | Denied |
| Media library | Allowed | Allowed | Denied | Denied |
| Photo gallery administration | Allowed | Allowed | Denied | Denied |
| Attendee/ticket exports | Allowed | Denied | Denied | Denied |
| Financial exports | Allowed | Denied | Denied | Denied |
| Legal pages | Allowed | Denied | Denied | Denied |
| FAQ administration | Allowed | Denied | Denied | Denied |
| Site settings | Allowed | Denied | Denied | Denied |
4. Admin permissions
4.1 Summary
admin is the only full-control role and the only role recognized as an administrator by the Better Auth admin plugin.
Source: backend/src/lib/betterAuth.ts:354-358
4.2 User administration
Admin can:
- list all users;
- view user statistics;
- view any user;
- update any user;
- assign any defined role;
- change email addresses;
- change account status;
- suspend accounts;
- delete non-admin users;
- access another user’s ticket history.
Relevant sources:
backend/src/routes/users.ts:29backend/src/routes/users.ts:93backend/src/routes/users.ts:120-122backend/src/routes/users.ts:151-219backend/src/routes/users.ts:227-229backend/src/routes/users.ts:261-279
Admin cannot delete an account while it still has the admin role, but it can first demote that account and then delete it.
There is no protection preventing:
- self-demotion;
- demotion of the final admin;
- a state in which no admin accounts remain.
4.3 Event administration
Admin can:
- view all events, including drafts;
- create events;
- update events;
- duplicate events;
- delete events;
- set a featured event;
- view event attendees;
- perform all door operations.
4.4 Ticket administration
Admin can:
- list all tickets;
- search and validate tickets;
- check in and undo check-in;
- mark cash payments paid;
- add administrative notes;
- create tickets;
- add attendees;
- cancel another user’s ticket;
- access privileged ticket details;
- download ticket and attendee exports.
4.5 Payment administration
Admin can:
- list all payments;
- view payment statistics;
- approve or reject payment submissions;
- place payments on hold;
- edit payment records where supported;
- refund payments;
- view and change global payment options;
- configure event-specific payment overrides;
- export financial data.
4.6 Communications and content
Admin can:
- manage the contacts inbox;
- list newsletter subscribers;
- read and send operational emails;
- create, update, and delete email templates;
- manage media;
- manage photo galleries;
- configure legal pages;
- manage FAQ content;
- manage site settings.
4.7 Important admin risks
- Role changes are not written to the existing
audit_logstable. - The final admin can be demoted.
- There is no event or tenant scope; all admin access is global.
- Direct database access can assign arbitrary role values.
5. Organizer permissions
5.1 Summary
organizer is a broad operations role. It is considered an application administrator by some frontend naming, but it is not a Better Auth administrator and cannot manage users or global platform configuration.
5.2 Allowed access
Organizer can:
- open the admin dashboard;
- view published and draft events;
- create, update, and duplicate events;
- view event attendees;
- use the full door scanner;
- search and validate tickets;
- check attendees in and undo check-in;
- create walk-ins and administrative tickets;
- mark cash payments paid;
- list tickets;
- approve and reject payments;
- configure event-specific payment overrides;
- manage contacts;
- read and send operational emails;
- manage media;
- manage Node and Go photo galleries.
5.3 Denied access
Organizer cannot:
- list or manage users;
- assign roles;
- delete events;
- access analytics;
- export attendees, tickets, or financial data;
- issue refunds;
- change global payment options;
- create, update, or delete email templates;
- manage legal pages;
- manage FAQ content;
- change global site settings.
5.4 Frontend mismatches
Payments page
The organizer is permitted to navigate to /admin/payments, but the page loads multiple resources together, including the full payment list. That list is admin-only.
Result:
- the organizer is authorized for payment approval operations;
- the page can still fail because one of its initial requests returns
403.
Relevant sources:
frontend/src/app/admin/payments/page.tsx:79-86backend/src/routes/payments.ts:37
Payment Options page
The organizer sees /admin/payment-options in navigation, but global payment options are admin-only.
Result:
- event-specific overrides are permitted;
- the global Payment Options page is not.
Relevant sources:
frontend/src/app/admin/payment-options/page.tsx:57backend/src/routes/payment-options.ts:85backend/src/routes/payment-options.ts:123backend/src/routes/payment-options.ts:273-284
Email templates
The organizer can read templates and send emails but cannot create, update, or delete templates. The frontend should clearly disable or hide those mutation controls.
Relevant sources:
backend/src/routes/emails.ts:61backend/src/routes/emails.ts:103backend/src/routes/emails.ts:149backend/src/routes/emails.ts:210
6. Staff permissions
6.1 Summary
staff is primarily a door and check-in role. Its strongest supported workflow is /admin/scanner.
The common door group is:
const STAFF_ROLES = ['admin', 'organizer', 'staff']
Source: backend/src/routes/door.ts:37
6.2 Allowed access
Staff can:
- enter the admin interface;
- use
/admin/scanner; - view the events list;
- view draft events through the events API;
- load door attendees;
- search attendees;
- resolve QR data used by the scanner;
- check attendees in;
- undo check-in;
- create walk-in attendees;
- view door summaries;
- view event attendees through the dedicated event attendee endpoint;
- search and validate tickets;
- add ticket notes;
- mark cash payments paid;
- create tickets administratively;
- add attendees through supported ticket operations.
6.3 Financial power at the door
Staff can mark payment as received:
ticketsRouter.post(
'/:id/mark-paid',
requireAuth(['admin', 'organizer', 'staff']),
...
)
Source: backend/src/routes/tickets.ts:1077-1079
The nearby comment says “admin only,” but the code permits organizer and staff. The code is authoritative; the comment is stale.
This is a meaningful financial permission because it can confirm tickets associated with a cash payment.
6.4 Denied access
Staff cannot:
- create, update, duplicate, or delete events through the API;
- list all tickets through the general ticket-list endpoint;
- administer payment approval queues;
- issue refunds;
- manage payment options;
- access exports;
- manage contacts;
- manage email;
- manage media or photo galleries;
- list or manage users;
- manage legal, FAQ, or site settings;
- cancel another user’s ticket.
6.5 Frontend mismatches
Event detail
The admin layout permits staff under /admin/events, including child paths. Event rows link to event detail pages.
The event detail data hook loads several resources in a single Promise.all, including:
- the full ticket list;
- email templates.
Those endpoints permit admin and organizer, not staff.
Result:
- staff can open the route;
- the event itself is visible;
- required page data returns
403; - the page may fail rather than showing a staff-safe subset.
Relevant sources:
frontend/src/app/admin/layout.tsx:63-78frontend/src/app/admin/events/[id]/_hooks/useEventDetailData.ts:19-25backend/src/routes/tickets.ts:1734backend/src/routes/emails.ts:61
Event CRUD controls
The events page shows controls for:
- creating events;
- editing events;
- deleting events;
- duplicating events;
- setting a featured event.
The corresponding APIs do not grant those powers to staff.
Result:
- staff sees actions it cannot complete;
- requests return
403; - deletion and featured-event actions are even narrower than create/update.
Scanner navigation
The scanner itself is correctly aligned with backend permissions. For a staff user, its back link goes to /admin/events.
Source: frontend/src/app/admin/scanner/page.tsx:67-68
That destination exposes the event-detail mismatch described above.
7. Marketing permissions
7.1 Summary
marketing is the narrowest staff role. Its implemented backend permissions are:
- newsletter subscriber list;
- privileged visibility of draft events;
- normal authenticated self-service access.
7.2 Allowed access
Marketing can:
- enter the admin layout;
- call the newsletter subscriber list endpoint;
- view published and draft events through the events API;
- view and update its own profile;
- access its own dashboard, tickets, and history.
Newsletter subscriber access:
requireAuth(['admin', 'marketing'])
Source: backend/src/routes/contacts.ts:252
7.3 Denied access
Marketing cannot:
- use the contacts inbox API;
- create or update events;
- use the scanner;
- list or administer tickets;
- administer payments;
- manage email templates;
- manage media or photo galleries;
- manage users;
- manage global settings.
7.4 Broken default experience
The admin layout:
- includes Contacts in marketing navigation;
- sends marketing users to
/admin/contactsby default.
The Contacts page calls GET /api/contacts, which allows only:
- admin;
- organizer.
Marketing is allowed to call /api/contacts/subscribers/list, but no frontend API wrapper or page currently uses that endpoint.
Result:
- marketing signs in;
- marketing is redirected to Contacts;
- the page’s primary request returns
403; - the one backend resource specifically allowed for marketing has no frontend.
Relevant sources:
frontend/src/app/admin/layout.tsx:63-88frontend/src/app/admin/contacts/page.tsx:26backend/src/routes/contacts.ts:181backend/src/routes/contacts.ts:252
8. Frontend route report
| Route | Admin | Organizer | Staff | Marketing | Effective condition |
|---|---|---|---|---|---|
/admin |
Works | Works | Redirected | Redirected | Dashboard API is admin/organizer |
/admin/events |
Works | Works | List works | Redirected | Staff sees unsupported actions |
/admin/events/[id] |
Works | Works | Broken/partial | Redirected | Staff dependencies return 403 |
/admin/scanner |
Works | Works | Works | Redirected | Door API aligned |
/admin/bookings |
Works | Works | Redirected | Redirected | Ticket list is admin/organizer |
/admin/users |
Works | Redirected | Redirected | Redirected | Admin-only |
/admin/payments |
Works | Broken/partial | Redirected | Redirected | Full list is admin-only |
/admin/payment-options |
Works | Broken | Redirected | Redirected | Global options are admin-only |
/admin/contacts |
Works | Works | Redirected | Broken | Marketing API mismatch |
/admin/emails |
Works | Partial | Redirected | Redirected | Organizer cannot mutate templates |
/admin/gallery |
Works | Works | Redirected | Redirected | Admin/organizer |
/admin/photos |
Works | Works | Redirected | Redirected | Admin/organizer |
/admin/legal-pages |
Works | Redirected | Redirected | Redirected | Admin-only |
/admin/faq |
Works | Redirected | Redirected | Redirected | Admin-only |
/admin/settings |
Works | Redirected | Redirected | Redirected | Admin-only |
/admin/tickets |
Works | Works | Shell opens, API fails | Shell opens, API fails | Missing from sidebar and route policy |
/dashboard |
Works | Works | Works | Works | Self-service for any authenticated user |
Orphan /admin/tickets route
/admin/tickets:
- exists;
- is linked from an admin dashboard statistic;
- is absent from the sidebar;
- is absent from the known-route permission list;
- can be opened by staff or marketing because the layout does not identify it as a disallowed known route;
- then fails to load the ticket list because the API permits only admin and organizer.
This should either be:
- added to navigation and explicit route permissions for admin/organizer; or
- removed and replaced with the intended bookings/event workflow.
9. Backend endpoint-level report
9.1 Events
| Method | Path | Allowed |
|---|---|---|
GET |
/api/events |
Public published events; admin/organizer/staff/marketing can see drafts |
GET |
/api/events/:id |
Public if published; privileged staff roles can view drafts |
POST |
/api/events |
Admin, organizer |
PUT |
/api/events/:id |
Admin, organizer |
DELETE |
/api/events/:id |
Admin |
GET |
/api/events/:id/attendees |
Admin, organizer, staff |
There is no event assignment or ownership condition. An organizer or staff member with permission sees all applicable events.
9.2 Door operations
| Method | Path | Allowed |
|---|---|---|
GET |
/api/events/:eventId/door-attendees |
Admin, organizer, staff |
POST |
/api/events/:eventId/door-checkin |
Admin, organizer, staff |
POST |
/api/events/:eventId/door-checkin/undo |
Admin, organizer, staff |
GET |
/api/events/:eventId/door-summary |
Admin, organizer, staff |
Source: backend/src/routes/door.ts
9.3 Tickets
| Method | Path/action | Allowed |
|---|---|---|
GET |
/api/tickets |
Admin, organizer |
GET |
/api/tickets/:id |
Public reduced response; privileged response for owner or staff trio |
GET |
/api/tickets/:id/pdf |
Public by ticket UUID; owner and staff also supported |
GET |
/api/tickets/booking/:bookingId/pdf |
Public by booking UUID |
POST |
Ticket check-in/search/validate/note/admin-create operations | Admin, organizer, staff |
POST |
/api/tickets/:id/mark-paid |
Admin, organizer, staff |
POST |
/api/tickets/:id/cancel |
Ticket owner or admin |
POST |
/api/tickets/:id/mark-payment-sent |
No session role or owner requirement |
Ticket and booking UUIDs are intentionally used as capability tokens for some emailed and payment flows. They must therefore be treated as secrets.
9.4 Payments
The payment API is split:
- full payment listing and selected aggregate views are admin-only;
- payment approval/rejection workflow permits admin and organizer;
- refunds are admin-only;
- public payment provider callbacks use their own validation.
This split is the reason the organizer Payments page can be visible but still fail during its combined data load.
9.5 Payment options
| Capability | Admin | Organizer | Staff | Marketing |
|---|---|---|---|---|
| View/update global payment options | Yes | No | No | No |
| View/update event overrides | Yes | Yes | No | No |
| Public event payment methods | Conditional public subset | Conditional public subset | Conditional public subset | Conditional public subset |
Detailed bank or TPago data may be returned when:
- the caller is admin/organizer; or
- a valid ticket ID for the event is supplied.
9.6 Contacts and subscribers
| Capability | Allowed |
|---|---|
| Contacts inbox CRUD | Admin, organizer |
| Subscriber list | Admin, marketing |
The frontend currently implements the first surface, not the second.
9.7 Email and media
| Capability | Allowed |
|---|---|
| Read email templates | Admin, organizer |
| Send operational email | Admin, organizer |
| Create/update/delete email templates | Admin |
| Upload/list/delete media | Admin, organizer |
| Photo API administration | Admin, organizer |
9.8 Platform configuration
Admin-only areas include:
- legal pages;
- FAQ administration;
- site settings;
- featured-event setting;
- global payment options;
- user administration;
- role assignment;
- refunds;
- analytics;
- exports.
10. Ownership and capability rules
10.1 Self-service dashboard
Any active authenticated role can use /api/dashboard/*, but queries are filtered to the current user.
This includes staff accounts acting as ordinary customers.
Source: backend/src/routes/dashboard.ts
10.2 User profiles
Non-admin users can view and update only their own profile. Role, email, and account-status fields are stripped or restricted unless the requester is admin.
Source: backend/src/routes/users.ts:120-219
10.3 User ticket history
Admin and organizer can access other users’ history. Staff, marketing, and ordinary users are limited to their own history.
Source: backend/src/routes/users.ts:227-229
10.4 Public ticket reads
GET /api/tickets/:id is public by ticket UUID, but personal information is reduced unless the caller is:
- the owner;
- admin;
- organizer;
- staff.
Source: backend/src/routes/tickets.ts:779-823
10.5 PDFs
Single-ticket and booking PDFs can be downloaded without a session when the caller knows the relevant UUID.
This supports emailed links, but creates capability-token risk:
- UUIDs must not appear in logs or analytics unnecessarily;
- links should not be shared;
- revocation and expiry should be considered for sensitive documents.
10.6 No event tenancy
There is no implemented relationship such as:
- organizer assigned to event;
- staff assigned to event;
- marketing assigned to campaign.
Role access is platform-wide. A staff user can operate on every event accepted by the door endpoints.
11. Security and governance findings
11.1 High: unauthenticated mark-payment-sent
POST /api/tickets/:id/mark-payment-sent:
- does not require a valid session;
- does not verify ticket ownership;
- relies on knowledge of the ticket UUID;
- can move a payment into
pending_approval.
Source: backend/src/routes/tickets.ts:1187-1317
Recommended correction:
- require the ticket owner’s session; or
- require a short-lived signed booking/payment token tied to the ticket and intended action.
Rate limiting alone does not establish authorization.
11.2 High on fresh deployments: first user becomes admin
The first user created in an empty database becomes admin:
const existing = await dbAll(
db.select().from(authUsers).limit(1)
);
if (!existing || existing.length === 0) {
return { data: { ...user, role: 'admin' } };
}
Source: backend/src/lib/betterAuth.ts:241-245
Risks:
- public registration may allow an unintended first registrant to gain full control;
- concurrent initial registrations may create a race;
- deployment order becomes security-sensitive.
Recommended correction:
- provision the initial admin explicitly through a deployment-only command or environment-gated bootstrap;
- disable automatic first-user elevation after initial setup.
11.3 Medium: no role-change audit
The database has an audit_logs table, but changing a role does not create an audit record.
Missing evidence includes:
- actor;
- target user;
- previous role;
- new role;
- timestamp;
- reason.
Recommended correction:
- write a transactional audit event whenever role or account status changes.
11.4 Medium: no final-admin guard
Admin accounts cannot be directly deleted while they remain admins, but they can be demoted. The system does not prevent the final admin from being demoted.
Recommended correction:
- count active admins before a demotion;
- reject changes that would leave no active admin;
- cover the rule with concurrent integration tests.
11.5 Medium: no database role constraint
The migration-defined role column is plain TEXT or varchar with no CHECK constraint or database enum.
Application validation protects ordinary API writes, but direct SQL, scripts, or faulty migrations can store arbitrary values.
Unknown roles generally fail closed at API allowlists, but can:
- break frontend assumptions;
- produce unusable accounts;
- complicate auditing;
- bypass intended reporting.
Recommended correction:
- add a database
CHECKconstraint or native enum; - validate existing values before applying the migration.
11.6 Review: public capability endpoints
The following are public or capability-based:
- ticket detail by UUID, with reduced PII;
- ticket PDF by UUID;
- booking PDF by UUID;
- LNbits ticket/payment status operations;
- payment details when a valid event ticket ID is supplied;
- media metadata by ID.
Some are deliberate customer flows, not necessarily defects. They should nevertheless be documented as capability-token interfaces and tested for data minimization.
11.7 Review: no event scope
Organizer and staff access is global. This is acceptable for a single operational team, but unsuitable if:
- third-party organizers are introduced;
- temporary event workers should see only one event;
- regional or franchise separation is required.
If event-scoped staff are anticipated, introduce explicit assignments before onboarding them.
12. Database and role-management observations
12.1 Schema inconsistency
The SQLite Drizzle declaration has a TypeScript enum annotation, while PostgreSQL uses a plain varchar. Neither migration enforces the allowed values at database level.
Sources:
backend/src/db/schema.ts:14backend/src/db/schema.ts:402backend/src/db/migrate.ts:16-22backend/drizzle/0000_steady_wendell_vaughn.sql:253
12.2 Role assignment
Only admin can assign roles through PUT /api/users/:id.
Source: backend/src/routes/users.ts:161-169
The frontend role dropdown applies changes immediately:
- no confirmation;
- no required reason;
- no audit record;
- no warning for granting admin;
- no final-admin protection.
Source: frontend/src/app/admin/users/page.tsx:129-136
12.3 Suspension behavior
Suspension:
- changes application account status;
- mirrors the Better Auth banned flag;
- removes active sessions;
- takes effect during session validation.
Source: backend/src/routes/users.ts:179-199
This is stronger than a frontend-only suspension.
13. Deployment and non-application access
Application roles do not protect:
- direct database access;
- Drizzle Studio;
- PostgreSQL administration;
- deployment environment files;
- service credentials;
- shell access as the deployment user.
Anyone with those privileges can bypass application RBAC and modify role data directly.
Operational access should therefore be managed separately through:
- OS accounts;
- SSH policy;
- database credentials;
- secret-management policy;
- production access logging.
The application report must not be interpreted as an infrastructure-access report.
14. Test coverage
Covered
- First user becomes admin.
- Subsequent users receive the default user role.
- Suspension/ban behavior.
- Staff door operations in backend integration tests.
- Selected photo API administrator behavior.
Relevant sources:
backend/src/lib/betterAuth.integration.test.ts:67-81backend/src/lib/betterAuth.integration.test.ts:127-135backend/src/routes/door.integration.test.tsphoto-api/internal/httpapi/api_test.go
Missing
There is no comprehensive authorization regression suite covering:
- every route against every role;
- marketing Contacts behavior;
- organizer Payments behavior;
- organizer Payment Options behavior;
- staff event-detail behavior;
- staff event CRUD rejection;
- final-admin protection;
- role-change auditing;
- malformed database roles;
- event-scoping expectations;
- capability-token data minimization.
There are no frontend tests verifying role-specific navigation or page behavior.
15. Recommended remediation plan
Priority 1: repair active authorization and workflow defects
- Protect
mark-payment-sentwith ownership or a signed action token. - Replace marketing’s Contacts landing page with a subscriber-focused page, or intentionally grant marketing the contacts scope.
- Make staff event detail load staff-safe endpoints only.
- Hide event create/edit/delete/duplicate/featured controls from staff.
- Split organizer Payments page data from admin-only payment listing.
- Remove organizer from global Payment Options navigation, or provide an event-overrides-only page.
- Add
/admin/ticketsto explicit route permissions and navigation, or remove it.
Priority 2: harden privilege administration
- Audit every role and account-status change.
- Prevent demotion of the last active admin.
- Add confirmation and clear privilege warnings to the role dropdown.
- Replace public first-user-admin behavior with explicit deployment provisioning.
- Add database constraints for allowed roles.
Priority 3: make the model maintainable
- Centralize role-group constants instead of repeating arrays.
- Document that the model is flat and uses exact allowlists.
- Add backend role-matrix integration tests.
- Add frontend navigation and page-load tests for every staff role.
- Document capability-token routes and their intended exposed fields.
- Decide whether organizer/staff should eventually be assigned to specific events.
16. Proposed official role policy
The following policy most closely matches current code intent after correcting the identified mismatches.
Admin
Full platform owner:
- users and roles;
- all event and ticket operations;
- all payments, refunds, and exports;
- all contacts, email, media, and galleries;
- legal, FAQ, payment, and site settings.
Organizer
Event operations manager:
- events except deletion;
- tickets and attendees;
- payment approval and event-level payment options;
- contacts;
- operational email;
- media and galleries;
- no user roles, refunds, exports, or global settings.
Staff
Door operator:
- scanner;
- attendees;
- check-in and undo;
- walk-ins;
- ticket validation;
- cash mark-paid;
- door summaries;
- read-only event context;
- no event administration or back-office payment administration.
Marketing
Subscriber operator:
- subscriber list;
- subscriber-related communications if later implemented;
- read-only event campaign context if required;
- no contacts inbox unless explicitly approved;
- no ticket, payment, user, or configuration administration.
User
Customer self-service:
- own profile;
- own dashboard;
- own tickets and payments;
- public booking and payment flows.
17. Key source index
| Purpose | Source |
|---|---|
| Canonical role declaration | backend/src/db/schema.ts:14 |
| PostgreSQL role declaration | backend/src/db/schema.ts:402 |
| Authentication middleware | backend/src/lib/auth.ts:38-90 |
| Better Auth admin configuration | backend/src/lib/betterAuth.ts:354-358 |
| First-user admin bootstrap | backend/src/lib/betterAuth.ts:241-245 |
| User role update workflow | backend/src/routes/users.ts:151-219 |
| Admin dashboard and exports | backend/src/routes/admin.ts |
| Event permissions | backend/src/routes/events.ts |
| Door permissions | backend/src/routes/door.ts |
| Ticket permissions | backend/src/routes/tickets.ts |
| Payment permissions | backend/src/routes/payments.ts |
| Payment option permissions | backend/src/routes/payment-options.ts |
| Contact and subscriber permissions | backend/src/routes/contacts.ts |
| Email permissions | backend/src/routes/emails.ts |
| Site settings | backend/src/routes/site-settings.ts |
| Frontend role flags | frontend/src/context/AuthContext.tsx:181-182 |
| Frontend admin navigation | frontend/src/app/admin/layout.tsx:63-114 |
| Next.js cookie middleware | frontend/src/middleware.ts:27-44 |
| Staff event-detail data load | frontend/src/app/admin/events/[id]/_hooks/useEventDetailData.ts:19-25 |
| Photo API administrator roles | photo-api/internal/auth/auth.go:31-48 |
| Production frontend nginx | deploy/prod_nginx/frontend.nginx.conf |
Conclusion
Spanglish has a workable role split, but it is implemented as many exact allowlists rather than one centralized permission system. The backend generally protects sensitive operations correctly, and the door role is reasonably well aligned with scanner APIs. The largest day-to-day problems are frontend/API drift for organizer, staff, and marketing.
The immediate goals should be:
- secure the unauthenticated payment-state endpoint;
- align each staff role’s pages with its backend permissions;
- audit and protect role administration;
- add role-matrix regression tests.
Until those changes are made, staff permissions should be understood from the backend matrix in this document, not solely from visible navigation or buttons.