Files
Spanglish/STAFF_PERMISSIONS_REPORT.md
T
MichilisandClaude Fable 5.1 a70333f5e4 Add the staff permissions audit report.
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>
2026-09-14 21:16:21 +00:00

37 KiB
Raw Blame History

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:

  1. admin
  2. organizer
  3. staff
  4. marketing

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 roles 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/tickets is 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-sent does 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 accountStatus is not active;
  • 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;
  • /api to the Node backend;
  • /uploads to 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 users 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 users ticket history.

Relevant sources:

  • backend/src/routes/users.ts:29
  • backend/src/routes/users.ts:93
  • backend/src/routes/users.ts:120-122
  • backend/src/routes/users.ts:151-219
  • backend/src/routes/users.ts:227-229
  • backend/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 users 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_logs table.
  • 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-86
  • backend/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:57
  • backend/src/routes/payment-options.ts:85
  • backend/src/routes/payment-options.ts:123
  • backend/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:61
  • backend/src/routes/emails.ts:103
  • backend/src/routes/emails.ts:149
  • backend/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 users 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-78
  • frontend/src/app/admin/events/[id]/_hooks/useEventDetailData.ts:19-25
  • backend/src/routes/tickets.ts:1734
  • backend/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/contacts by 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 pages primary request returns 403;
  • the one backend resource specifically allowed for marketing has no frontend.

Relevant sources:

  • frontend/src/app/admin/layout.tsx:63-88
  • frontend/src/app/admin/contacts/page.tsx:26
  • backend/src/routes/contacts.ts:181
  • backend/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 owners 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 CHECK constraint 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:14
  • backend/src/db/schema.ts:402
  • backend/src/db/migrate.ts:16-22
  • backend/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-81
  • backend/src/lib/betterAuth.integration.test.ts:127-135
  • backend/src/routes/door.integration.test.ts
  • photo-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.


Priority 1: repair active authorization and workflow defects

  1. Protect mark-payment-sent with ownership or a signed action token.
  2. Replace marketings Contacts landing page with a subscriber-focused page, or intentionally grant marketing the contacts scope.
  3. Make staff event detail load staff-safe endpoints only.
  4. Hide event create/edit/delete/duplicate/featured controls from staff.
  5. Split organizer Payments page data from admin-only payment listing.
  6. Remove organizer from global Payment Options navigation, or provide an event-overrides-only page.
  7. Add /admin/tickets to explicit route permissions and navigation, or remove it.

Priority 2: harden privilege administration

  1. Audit every role and account-status change.
  2. Prevent demotion of the last active admin.
  3. Add confirmation and clear privilege warnings to the role dropdown.
  4. Replace public first-user-admin behavior with explicit deployment provisioning.
  5. Add database constraints for allowed roles.

Priority 3: make the model maintainable

  1. Centralize role-group constants instead of repeating arrays.
  2. Document that the model is flat and uses exact allowlists.
  3. Add backend role-matrix integration tests.
  4. Add frontend navigation and page-load tests for every staff role.
  5. Document capability-token routes and their intended exposed fields.
  6. 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:

  1. secure the unauthenticated payment-state endpoint;
  2. align each staff roles pages with its backend permissions;
  3. audit and protect role administration;
  4. 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.