Skip to content

Operate The App On Laravel Cloud

This app is hosted on Laravel Cloud, which means there is no SSH box to log into for a quick artisan command or psql session — everything goes through Laravel Cloud's HTTP API, and the runtime itself is a horizontally scaled, containerized environment with no shared local filesystem between requests. That last fact is not a footnote: it has already caused two separate production incidents in this app, both from code that assumed a file written in one request would still be there for a later one.

Running Artisan commands and querying the database remotely

Credentials for this must never live in the repo. CLAUDE.md documents sourcing them from a local, gitignored file instead:

# ~/.config/yaya-engine/secrets.env  (gitignored, chmod 600)
export YAYA_CLOUD_ENV_ID="env-..."
export YAYA_CLOUD_API_TOKEN="..."
export YAYA_PROD_DB_HOST="...pg.laravel.cloud"
export YAYA_PROD_DB_PASSWORD="..."

[@claude-md]. With that sourced, an Artisan command is submitted and polled in two separate calls:

curl -s -X POST "https://cloud.laravel.com/api/environments/${YAYA_CLOUD_ENV_ID}/commands" \
  -H "Authorization: Bearer ${YAYA_CLOUD_API_TOKEN}" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"command": "php artisan <command>"}'

curl -s "https://cloud.laravel.com/api/commands/<command-id>" \
  -H "Authorization: Bearer ${YAYA_CLOUD_API_TOKEN}" -H "Accept: application/json"

[@claude-md]. Direct Postgres access uses the same sourced credentials against Neon:

PGPASSWORD="${YAYA_PROD_DB_PASSWORD}" psql -h "${YAYA_PROD_DB_HOST}" -U laravel -d main

[@claude-md]. Per CLAUDE.md, deploys run automatically on push, and php artisan migrate --force runs automatically as part of every deploy — you should not need to run migrations manually in normal operation [@claude-md]. Application logs live in Laravel Cloud's log viewer, not in storage/logs/ on any single container, since there is no single container to read logs from [@claude-md]. If a secret is ever committed to the repo, rotate it first and only then purge it from git history — rotation without history purging leaves the leaked value valid [@claude-md].

Gotcha: Passport keys must be set as environment variables

config/passport.php reads its signing keys from environment variables and falls back to Passport's default file-based keys otherwise:

'private_key' => env('PASSPORT_PRIVATE_KEY'),
'public_key' => env('PASSPORT_PUBLIC_KEY'),

[@passport-config]. Locally, if these env vars are unset, Passport silently falls back to storage/oauth-private.key / storage/oauth-public.key on disk — those files are gitignored and never deployed. On Laravel Cloud, the filesystem is ephemeral and the app may be running across multiple instances, so file-based keys either don't persist across deploys or differ per instance; either way, token minting breaks. The design doc for MCP personal-access-token minting states this plainly: without the env vars set, createToken() throws LogicException: Invalid key supplied from CryptKey.php [@mcp-token-minting-plan].

The fix is to generate a real key pair once and set it as configuration, never as files on the deployed filesystem:

php artisan passport:keys --force   # generates storage/oauth-private.key + oauth-public.key locally

then paste the full PEM contents (including the BEGIN/END lines) of the generated oauth-private.key into PASSPORT_PRIVATE_KEY, and oauth-public.key into PASSPORT_PUBLIC_KEY, per environment, and redeploy [@mcp-token-minting-plan]. This is not optional for any environment that needs to mint Passport tokens — which includes minting MCP machine-client tokens; see MCP Token Minting for why that flow needs Passport personal-access tokens at all, and Auth Architecture for where Passport sits relative to the other two auth guards.

Gotcha: never assume a file written in one request survives to the next

A second, independently-discovered instance of the same ephemeral-filesystem fact broke a Yaya Manager Excel export button in production. The original flow saved a generated .xlsx file to local storage in a POST request, returned a download_url in the JSON response, and had the frontend open that URL in a new tab as a second, separate GET request [@excel-export-doc]. On Laravel Cloud's containerized, horizontally-scaled infrastructure, the POST and the follow-up GET are not guaranteed to hit the same container — when they didn't, the file simply didn't exist where the second request looked for it, producing an intermittent 404 that worked fine locally and failed unpredictably in production [@excel-export-doc].

The fix collapsed the two-request pattern into one: the export endpoint became a single GET route that generates the file and streams it back directly with response()->download(...)->deleteFileAfterSend(), so generation and delivery happen in the same request on the same container, with no intermediate storage step and no orphaned temp file left behind [@excel-export-doc]. The route also changed from POST to GET, since a direct browser navigation is what actually triggers a file download correctly, and the export is idempotent so GET is semantically appropriate [@excel-export-doc].

The general lesson from this incident, worth applying to any new feature that generates a downloadable file: never save a file in one request and read it back in a later one on this platform. Stream a single-request response, or push the file somewhere shared (like S3) if generation truly needs to be decoupled from delivery.