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:
[@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:
[@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:
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.