Panic Backstage helps venues track every show from hold to settlement, including lineup, schedule, artwork, ticketing, open items, and event readiness.
The app is intentionally boring to run:
- PHP 8, served from
public/ - no Composer runtime dependencies
- no npm, bundler, or frontend build step
- MySQL with native PDO prepared statements
- native PHP sessions, password hashing, CSRF tokens, and file uploads
- static HTML/CSS/native Web Components that call JSON endpoints under
/api - LARC/PAN loaded from a pinned CDN module for component coordination
Panic Backstage is API-first. PHP endpoint classes return JSON only; HTML pages are static and use browser-native Web Components, fetch(), and LARC/PAN topic events to load and mutate data.
The custom kernel in src/Kernel.php resolves constrained API paths to endpoint classes:
GET /api/dashboard -> src/Dashboard.php
GET /api/events -> src/Events.php
POST /api/events -> src/Events.php
GET /api/events/{id} -> src/Events.php
PATCH /api/events/{id} -> src/Events.php
GET /api/events/{id}/tasks -> src/Events/Tasks.php
POST /api/events/{id}/tasks -> src/Events/Tasks.php
PATCH /api/events/{id}/tasks/{taskId} -> src/Events/Tasks.php
DELETE /api/events/{id}/tasks/{taskId} -> src/Events/Tasks.php
GET /api/events/{id}/assets -> src/Events/Assets.php
POST /api/events/{id}/assets -> src/Events/Assets.php
PATCH /api/events/{id}/assets/{id} -> src/Events/Assets.php
GET /api/public/events/{slug} -> src/PublicEvents.php
Each endpoint receives a Request, returns a Response, and uses shared services such as Database and Auth.
public/
index.html Main staff UI
login.html Login page
event.html Public event page shell
invite.html Invite acceptance shell
router.php Local dev router for PHP built-in server
.htaccess Apache rewrite for /api
api/index.php API entrypoint
assets/app.css Venue-ops UI styling
assets/app.js Web Components client
uploads -> ../storage/uploads
src/
bootstrap.php Autoloader and shared function include
Kernel.php API path resolver and request dispatch
Request.php HTTP request wrapper
Response.php JSON response wrapper
Database.php PDO wrapper
Auth.php Native session auth and CSRF
Support.php Small helper functions
Dashboard.php
Events.php
Templates.php
PublicEvents.php
Invites.php
Events/
Tasks.php
Blockers.php
Lineup.php
Schedule.php
Assets.php
Settlement.php
Invites.php
database/
schema.sql
seed.php
storage/
uploads/events/
schema.sql Root copy of database schema
.env.example Environment variable template
- PHP 8.2 or newer
- MySQL 8 or newer
- PHP PDO MySQL extension
No Composer or Node install is required.
The frontend uses this pinned LARC module directly in the browser:
<script type="module" src="https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs"></script>Copy the example environment file:
cp .env.example .envSet the database credentials in .env:
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=panic_backstage
DB_PASSWORD=your-local-password
DB_NAME=panic_backstage
If you need to create the local MySQL user manually:
CREATE DATABASE IF NOT EXISTS panic_backstage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'panic_backstage'@'localhost' IDENTIFIED BY 'your-local-password';
CREATE USER IF NOT EXISTS 'panic_backstage'@'127.0.0.1' IDENTIFIED BY 'your-local-password';
GRANT ALL PRIVILEGES ON panic_backstage.* TO 'panic_backstage'@'localhost';
GRANT ALL PRIVILEGES ON panic_backstage.* TO 'panic_backstage'@'127.0.0.1';
FLUSH PRIVILEGES;Then seed the database:
php database/seed.phpSeed admin login:
email: admin@mabuhay.local
password: changeme
Use PHP's built-in server:
php -S localhost:8000 -t public public/router.phpOpen:
http://localhost:8000
The built-in router serves static files normally and dispatches /api/* to public/api/index.php.
To smoke-test the app under a subdirectory path, set APP_BASE_PATH:
APP_BASE_PATH=/backstage php -S localhost:8000 -t public public/router.phpThen open http://localhost:8000/backstage/. Static assets, API calls, invite links, and uploaded media are resolved relative to the app base path instead of the server document root.
The seeded Mabuhay Gardens demo is designed for a venue operations walkthrough.
Reset and launch locally:
php database/seed.php
php -S localhost:8000 -t public public/router.phpLogin:
email: admin@mabuhay.local
password: changeme
Suggested flow:
- Open the dashboard and call out the next show, open items, empty holds, flyer needs, and settlement signals.
- Open
Local Band Showcase, edit event details, assign tasks, resolve the open flyer/ticketing items, approve the seeded flyer, and review the run sheet. - Create an invite link from the event workspace and copy it for a collaborator.
- Use the public page button after publishing the event to show the guest-facing event page.
- Open Templates, create a new show from
Three-Band Local Show, then show how tasks and schedule items come preloaded. - Open
Legacy Benefit Night, calculate venue net, and show the completed settlement fields. - Open Calendar and Pipeline to show the same events by date and update operational status.
Example public event URL after seeding:
http://localhost:8000/event.html?slug=local-band-showcase
Endpoint smoke test against a running local or staging server:
php scripts/endpoint-smoke.php http://localhost:8000The smoke script logs in as admin, loads dashboard data, creates an event from a template, updates an open item, creates and accepts a viewer invite, verifies collaborator event access, verifies unrelated event access and viewer mutation are blocked, saves settlement data, publishes the event, and verifies the public event API. It triggers real invite and magic-link emails through the Mailer (each written to storage/mail/ and piped to the system MTA); it does not exercise multipart asset upload.
Use public/ as the web root.
For Apache, the included public/.htaccess routes API requests to public/api/index.php.
For Nginx/PHP-FPM, route /api/* to public/api/index.php and serve static files from public/. If the app is mounted under a subdirectory such as /backstage, route that prefix to public/ and set APP_BASE_PATH=/backstage if the server does not expose the prefix through SCRIPT_NAME. Uploads should resolve through the public/uploads symlink to storage/uploads.
Keep .env outside version control. It is ignored by .gitignore.
Staging checklist:
- PHP 8.2+, PDO MySQL, and fileinfo enabled.
.envuses staging database credentials andAPP_BASE_PATHwhen mounted below/.storage/uploads/eventsis writable by the PHP process.- The server can reach
https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs, or LARC should be vendored in a future offline-demo pass. - Run
php database/seed.phponly when resetting demo data is acceptable.
Events stay in sync with the MabEvents Tracker Google Sheet in both directions: an inbound cron imports the sheet every 5 minutes, and app edits are pushed back up to the sheet in real time (with a cron-based retry fallback). Outbound writes authenticate as a Google service account.
See docs/google-sheet-sync.md for setup
(service-account key, sharing, permissions), the field/column mapping, and
troubleshooting.
Transactional email is handled by src/Mailer.php. It builds an RFC 5322
message and pipes it to the system /usr/sbin/sendmail interface (the
sendmail-compatible front end to the host MTA, e.g. Exim). Every message is
also written to storage/mail/*.eml for local inspection, and any delivery
failure is appended to storage/mail/_delivery-errors.log rather than thrown,
so a mail problem never breaks an auth or invite flow.
Email is sent for:
- Collaborator invites (with a per-invite resend action).
- Login / magic links and the admin welcome link.
Relevant environment variables:
APP_URL=https://panicbooking.com/backstage # base used to build invite/login links
MAIL_FROM_ADDRESS=support@panicbooking.com
MAIL_FROM_NAME=Backstage
MAIL_BCC= # optional, comma/semicolon-separated blind copies
Creating an invite sends the email by default. The Invites form exposes a
Send invitation email checkbox (checked by default); unchecking it generates
the invite link without sending, so an admin can copy and share it manually. The
POST /api/events/{id}/invites endpoint mirrors this with a send_email flag.
Deliverability to external inboxes depends on the host MTA plus SPF/DKIM/DMARC
for the sending domain. Inspect storage/mail/ and the host MTA log to confirm
a given message was generated and accepted.
/shows the staff dashboard after login.- Venue admins can create events from templates and see all events.
- Each event workspace manages overview, lineup, tasks, open items, run sheet, assets, settlement, and activity.
- Public event pages are loaded by
public/event.html?slug=event-slug. - Public event API responses only include events with
public_visibilityenabled. - Web Components publish PAN-compatible topics such as
app.route.changed,events.loaded,event.saved,event.assetUploaded,event.openItemResolved,event.publicationChanged,toast.show, andapi.error.
Server-side authorization is enforced by global user role plus event ownership or event_collaborators rows. Venue admins retain full access to every event, template, invite, asset, settlement, and user list. Non-admin users only see events they own or events where they have a collaborator row.
Event collaborator roles:
event_owner: full access to assigned/collaborating events except global user or template administration.promoter: read event data, edit lineup, tasks, schedule, and open items, and view/copy the public page. Settlement is hidden.band/artist: read the collaborating event, upload assets, and view tasks assigned directly to them.designer: read the collaborating event and upload/manage event assets. Settlement is hidden.staff: read the collaborating event and edit tasks, schedule, and open items.viewer: read-only access to the collaborating event.
Creating an invite emails the recipient an accept link by default, and each pending invite can be re-sent. The link is also shown in the UI to copy and share manually (see Email).
After logging in as the seeded admin:
- Open an event workspace and select the Invites tab.
- Create a viewer, staff, designer, promoter, artist, or band invite. Leave Send invitation email checked to email the link, or uncheck it to just generate a copyable link.
- Use the emailed link, or copy the generated invite link, and open it in a separate browser session or private window.
- Accept the invite with a name and password.
- Confirm the collaborator can open that event from the dashboard or direct event URL.
- Confirm unrelated events are not listed and direct access to unrelated event IDs is rejected.
- For a viewer invite, confirm event detail fields, settlement, invite creation, and destructive asset controls are unavailable.
- For a designer invite, manually verify asset upload/manage controls on that event. The smoke script does not exercise multipart uploads.
Useful checks:
find src public database scripts -name '*.php' -print -exec php -l {} \;
node --check public/assets/app.js
php database/seed.php
php -S localhost:8000 -t public public/router.php
php scripts/endpoint-smoke.php http://localhost:8000node --check is optional and only validates the plain JavaScript file. The app does not require Node to run.
- Stripe is represented by ticket fields only.
- Email delivery relies on the host MTA via
/usr/sbin/sendmail; there is no queue, retry, or bounce handling beyond what the MTA provides. - Uploads use local disk storage under
storage/uploads/events/:eventId. - The frontend is intentionally browser-native Web Components, optimized for hackability over framework features.