# Getting off the simulated JSON backend and onto your real airbridgelabs.com server

This is the full checklist for moving this app from "local demo, JSON file
storage" to "actually running on your cPanel server at airbridgelabs.com,
backed by real MySQL." It covers everything, not just the database: which
files need to be transferred, how the web server needs to be configured
to serve them, PHP settings, and the database hookup.

**Production (your real server) and Local (your Mac, for testing) are
fully separate sections below -- do not mix steps between them.** If
you're only interested in going live, skip straight to Production and
ignore the Local section entirely; it's there only for if you want a
sandbox to test changes before pushing them to the real server.

---

## PRODUCTION -- deploying to airbridgelabs.com

You've already done part of this: created the `airbridg_photo_slots`
database in cPanel, created and associated a database user, and run
`server/sql/schema.sql` against it (creating the `instances`,
`instance_members`, `realtime_events`, and `app_state` tables). That
covers steps 1-2 below. What's still needed is transferring the
application files themselves, telling Apache how to serve them, and
pointing PHP at the database you already set up.

### 1. The database (already done)

For reference/future instances, this was:

1. cPanel -> **MySQL Database Wizard** -> create database
   (`airbridg_photo_slots`) -> create user -> attach user to database with
   **All Privileges**.
2. cPanel -> **phpMyAdmin** -> select the database -> **Import** tab ->
   choose `server/sql/schema.sql` -> **Go**.

This is a one-time step for the whole database -- it's never re-run per
wedding/birthday/instance. Every future instance is just a new row the
app itself creates.

### 2. What files need to go on the server

This app is a monorepo, and its PHP router (`server/public/index.php`)
serves the guest app, the admin panel, and every shared JS package
directly off disk using paths relative to its own location -- it does
**not** expect those files to be duplicated into the web root separately.
That means the entire folder structure has to be uploaded together,
including two sibling folders outside `Events/Weddings` itself:

```
Airbridge/                                <- everything below travels together
  Events/
    Weddings/                             <- this repo (apps/, packages/, server/, ...)
  Shared platform services/
    Universe identity and membership/
      local-universe.js                   <- required (PlatformHost/settings-gear logic)
  App Creator/
    index.html, creator.js, creator.css   <- required only if you use the /creator route
```

**Where to put it on the server:** don't put this inside `public_html`
directly -- that would make `server/src`, `server/config/database.php`
(your real DB password), and `server/storage` directly downloadable by
anyone who guesses the URL. Instead:

1. Create a folder outside your web root, e.g.
   `/home/airbridg/app/Airbridge/` (exact path depends on your cPanel
   account -- File Manager's "Home" is a good reference point for where
   `public_html` sits, so a sibling folder like `~/app` works well).
2. Upload the entire `Airbridge` folder there (FTP/SFTP, or zip it
   locally and use cPanel File Manager's Upload + Extract). Preserve the
   exact folder structure shown above.
3. In cPanel -> **Domains** (or **Subdomains**, if airbridgelabs.com's
   Photo Slots app lives on a subdomain), find the entry for the domain
   this app should answer on, and set its **Document Root** to:
   `/home/airbridg/app/Airbridge/Events/Weddings/server/public`
   (adjust the base path to wherever you actually uploaded it). This is
   the one folder that needs to be web-reachable -- everything else stays
   private, and `index.php` reaches out to the sibling folders itself
   using PHP file operations, not URLs, so they never need to be inside
   any document root.

### 3. Add the front-controller rewrite

`server/public/.htaccess` has already been created for you in this repo,
so it travels with everything else when you upload. It tells Apache to
route every request through `index.php` (PHP's built-in dev server you've
been testing with locally does this automatically, which is why it was
never needed before now -- Apache has no equivalent built in). Nothing
to configure here, just make sure the `.htaccess` file actually made it
into `server/public/` on the server (some FTP clients hide dotfiles by
default -- double check "show hidden files" is on).

### 4. PHP requirements

Check these in cPanel's **MultiPHP Manager** / **Select PHP Version**
(for the domain from step 2) and **MultiPHP INI Editor**:

- **PHP 8.1 or newer**, matching what this codebase targets
  (`declare(strict_types=1)` + constructor property promotion throughout).
- **`mysqli` extension enabled** -- in **Select PHP Version**'s extension
  checklist for this domain. On by default on almost all cPanel hosting.
- **`gd` extension enabled** -- used for the reel sprite/image compositing
  (`ReelSpriteService`, `AssetService`). Also on by default almost
  everywhere, but worth confirming.
- **Upload size limits** -- a `server/public/.user.ini` file is already
  included and sets `upload_max_filesize`/`post_max_size`/
  `max_file_uploads`/`memory_limit` automatically if your account runs
  PHP-FPM (the current cPanel default). If uploads of the couple's
  welcome video still fail with a size error, open **MultiPHP INI
  Editor**, select this domain, and set `upload_max_filesize` to `160M`
  and `post_max_size` to `170M` directly -- that always works regardless
  of PHP execution mode.

### 5. Point PHP at your database

Edit (or create) `server/config/database.php` **directly on the server**
(it's gitignored, so it never travels with any file transfer that goes
through git -- upload/edit it individually via File Manager, SFTP, or
cPanel's built-in file editor). Start from `database.example.php` if it's
not there yet, and fill in the real values from step 1:

```php
return [
    'host' => getenv('DB_HOST') ?: '127.0.0.1',
    'port' => (int) (getenv('DB_PORT') ?: 3306),
    'database' => getenv('DB_NAME') ?: 'airbridg_photo_slots',
    'username' => getenv('DB_USER') ?: 'airbridg_ceri',
    'password' => getenv('DB_PASSWORD') ?: 'YOUR_REAL_PASSWORD',
];
```

(`host` is almost always `127.0.0.1` or `localhost` on shared cPanel
hosting -- cPanel's database wizard will have told you if yours is
different.)

**Nothing else is needed to switch the app onto MySQL.** `index.php`
automatically uses MySQL the moment it sees a real `server/config/
database.php` file present -- no environment variable or extra step
required. (An explicit `DB_DRIVER` environment variable still works as an
override if you ever want to force one mode or the other, but you don't
need to set one for the normal case.)

### 6. Bring over your existing data (optional)

If you want the wedding(s) you've been testing with locally (e.g.
"demo-wedding") to exist on the production database too, upload your
local `server/storage/dev-database.json` to the same path on the server,
then run from an SSH terminal (cPanel -> **Terminal**, if enabled, or ask
your host to enable SSH access):

```
php server/scripts/migrate_json_to_mysql.php
```

This is a one-time import -- it reads that JSON file and writes its
contents into MySQL using the same `write()` method the live app uses for
every save afterward. If you'd rather start production completely fresh
(no imported test data), skip this step entirely -- the app will just
start with an empty `instances` table and whoever sets up the first real
wedding through the admin panel creates the first row.

### 7. Verify

Visit the domain in a browser and confirm:

- The guest app loads at the domain root (reels visible, no console
  errors about missing `/packages/...` or `/airbridge/shared/local-
  universe.js` files -- a 404 on either means the sibling folders from
  step 2 didn't make it over, or the document root is pointed at the
  wrong folder).
- `/admin/?wedding=demo-wedding` (or whichever `publicId` you're using)
  loads the admin panel and the Admin Token field works.
- In phpMyAdmin, spot-check the data actually landed:
  ```sql
  SELECT public_id, category, subcategory, status FROM instances;
  SELECT COUNT(*) FROM instance_members;
  ```

### 8. Media storage

Uploaded guest photos/videos and PlatformHost-provisioned assets are stored as
real files on disk (not in the database) under `server/storage/media`.
Make sure that folder exists on the server and is writable by the PHP
process (cPanel accounts typically run PHP as your own account user via
PHP-FPM, so normal file ownership from your upload already covers this;
if you get "permission denied" errors on upload, `chmod -R 755
server/storage/media` from Terminal or File Manager's permissions dialog
is the usual fix). This folder holds real content going forward -- back
it up like any other user-generated data, and don't delete it between
deployments.

### Rolling back

If something looks wrong after switching to MySQL, temporarily rename or
delete `server/config/database.php` -- with it gone, `index.php`
immediately falls back to the JSON file (`server/storage/dev-database.json`)
exactly as before. Nothing about this migration is destructive to that
file, so it remains a safe fallback for as long as you want to keep it.

---

## LOCAL -- testing on your Mac (optional, separate from the above)

Everything in this section is for your own machine only. None of it
applies to or is needed for the production server above.

### Running the app locally

```
php -d upload_max_filesize=160M -d post_max_size=170M -S localhost:8080 -t server/public
```

This is what you've been using throughout development. PHP's built-in
server automatically routes any URL that doesn't match a real file to
`index.php` in the `-t` folder, which is why no `.htaccess` was ever
needed locally.

### Testing MySQL locally before touching production (optional)

If you want to try the MySQL storage path locally first:

1. Install MySQL: `brew install mysql && brew services start mysql`
2. Create a local database and user:
   ```
   mysql -u root -e "CREATE DATABASE photo_slots; CREATE USER 'photo_slots_user'@'localhost' IDENTIFIED BY 'change-me'; GRANT ALL ON photo_slots.* TO 'photo_slots_user'@'localhost';"
   ```
3. Load the schema: `mysql -u root photo_slots < server/sql/schema.sql`
4. Edit `server/config/database.php` locally with those local credentials.
5. Run `php server/scripts/migrate_json_to_mysql.php` to copy your local
   `dev-database.json` into it.
6. Reload the local app -- since `server/config/database.php` now exists
   locally too, it automatically switches to MySQL the same way
   production does. Delete/rename that file locally to go back to the
   JSON file.

This is entirely independent of your production database -- local MySQL
and the `airbridg_photo_slots` database on airbridgelabs.com never talk to
each other. It's just a way to rehearse the same switch-over on a
throwaway local database before doing it for real.

---

## Optional: automatic replay export when an event ends

If you set an "Event ends" date/time in the admin panel's Event Schedule
fields, the wedding can close itself and generate its offline replay bundle
automatically once that time passes -- no PlatformHost action required. This
needs one cPanel Cron Job, since PHP scripts don't run on their own:

1. cPanel -> **Cron Jobs**.
2. Add a new cron running every 15 minutes (or hourly -- there's no harm in
   checking more often than events actually end):
   ```
   */15 * * * * php /home/airbridg/app/Airbridge/Events/Weddings/server/scripts/auto_close_expired_events.php >> /home/airbridg/app/Airbridge/Events/Weddings/server/storage/auto_close.log 2>&1
   ```
   (adjust the path to wherever you actually uploaded the app -- same base
   path as the Document Root you set in step 2 above, just pointed at
   `server/scripts/` instead of `server/public/`.)
3. That's it -- the script is safe to run as often as the cron fires: it
   only acts on weddings with an eventEndsAt set that's actually in the
   past and not already closed, and does nothing to everything else.

A PlatformHost can still close a wedding and/or generate its replay export
manually at any time (Close button / Export Replay button in the admin
panel) regardless of whether this cron is set up at all -- this is purely
an optional convenience so a PlatformHost doesn't have to remember to do it
themselves right at the event's end time.

## Adding your next instance (wedding, birthday, or otherwise)

Once live on MySQL, creating a new instance is exactly what it is today --
go through the app's normal setup flow. That's just one more row in
`instances`. No SQL, no new database, no re-running `schema.sql`, on
either environment.

## Requirements recap

- PHP 8.1+, `mysqli` and `gd` extensions enabled.
- MySQL 5.7+ or MariaDB 10.2+ (JSON column type support).

## A note on security

`server/config/database.php` holds a real password -- it's gitignored on
purpose. Don't paste its contents into chat, commit it, or store it
anywhere outside the server itself. The database user from step 1 should
only ever have privileges on this one database, never your cPanel
account's main/root MySQL user.
