# Security and backend quick wins

Cross-site writes to the API are refused, production stops trusting the development app scheme, purge needs an owner or admin, and jobs and migrations fail more safely.

- **Cross-site writes to `/api/rpc` are refused.** A write whose `Origin` is neither the request's origin nor `BETTER_AUTH_URL`'s now gets HTTP 403. So does a write with no `Origin` that `Sec-Fetch-Site` marks as `cross-site` or `same-site`. This covers the OpenAPI mirror. A PR preview on a sibling subdomain can no longer write with a production cookie. The native app, scripts and server-to-server calls send neither header and are unaffected. See [API](/docs/features/api).
- **Production no longer trusts `<scheme>-dev://`.** No production app registers the development build's scheme, so any app could claim a password-reset token sent there. The scheme is trusted in development, and on previews through the new preview-only switch `NATIVE_DEV_SCHEME=1`. Set it in `apps/web/.dev.vars` to sign a development build into a local built Worker. See [Runtime switches](/docs/development/environments#runtime-switches).
- **Only owners and admins can purge the trash,** and purge now checks the database session. The files gateway also no longer reuses a snapshot-authorized tenant where it asked for the database session. See [Files](/docs/features/files).
- **`BETTER_AUTH_SECRET` must be at least 32 characters.** `pnpm setup` already generates 44. A shorter secret now fails the production deploy before migrations, and the preview secrets step refuses it, so the running Worker is never replaced by one that refuses to boot. Changing a short secret signs every user out.
- **The cron tick is observable.** Each failed job logs one `jobs.step_failed` line with the type and code of its cause, never a message. Every tick logs one `jobs.tick` line: settled with counts, interrupted by the deadline, or failed. See [Scheduled jobs](/docs/features/scheduled-jobs).
- **Job and migration lock waits end on the server.** The job pool's `lock_timeout` (1 s) and `statement_timeout` (1.5 s) now fire before its 2 s client timeout. A claim reported as failed can no longer commit later. The migration runner takes a transaction-scoped advisory lock per folder, which is safe on a pooled connection, and gives up a table lock after 5 seconds instead of queueing traffic behind it. See [Database](/docs/features/database).

[PR #201](https://github.com/CarlosZiegler/bjj-travel/pull/201)

