Skip to content

feat(server): deploy the API to Serverpod Cloud with health version and CORS gate - #115

Merged
moises-cisneros merged 13 commits into
chore/110-serverpod-4-upgradefrom
chore/31-serverpod-cloud-deploy
Oct 8, 2026
Merged

moises-cisneros merged 13 commits into
chore/110-serverpod-4-upgradefrom
chore/31-serverpod-cloud-deploy

Conversation

@TOMOKI977

Copy link
Copy Markdown
Contributor

Refs #31

Stacked on #114 (Serverpod 4 upgrade). Review and merge #114 first; this PR will then be retargeted to main. Stacked PRs do not run CI, so CI runs once it targets main.

Summary

Server side of #31: the API is live on Serverpod Cloud at https://puls3-hub-on-stellar.api.serverpod.space/.

  • Health reports the deployed commit. /health/check returns version from the pubspec plus PULS3_GIT_SHA. A pre-deploy script (tool/set_deploy_sha.dart, run by scloud deploy through scloud.yaml) sets that variable to the deployed short SHA, with -dirty added when tracked files have uncommitted changes.
  • CORS origin gate. Browser callers are restricted to PULS3_ALLOWED_ORIGINS. Loopback origins are allowed only in the development run mode, so the deployed server rejects localhost.
  • Serverpod Cloud project config. scloud.yaml is linked to puls3-hub-on-stellar, and .scloudignore keeps .env, passwords.yaml and tests out of the upload.
  • README "Deploy to Serverpod Cloud". It gives one linear first-deploy path, then redeploy, verify and failure handling, plus the public API URL.

Acceptance criteria

  • The public app URL opens and the full demo flow works on testnet (GIF). Not in this PR: the Flutter web deploy to Cloudflare Pages is still pending.
  • The health endpoint on the public server returns the deployed version (1.0.0+b88cdaf, below).
  • A teammate who did not do the first deploy redeploys by following the README. Needs a teammate: the redeploy steps are in puls3_server/README.md.
  • No secret appears in the repo or in the upload. scloud deploy --wet-run --show-files lists .env and config/passwords.yaml as ignored, and the Cloud-managed passwords are generated by the platform.
  • Both URLs are in the README. The API URL is in puls3_server/README.md; the app URL comes with the Pages deploy.

Deliverable 6 (the indexer/tracker running on the deployed server) is not enabled: PULS3_TRACKER_ENABLED is left unset until #96.

Verification evidence

$ scloud deploy
(1/1) dart run tool/set_deploy_sha.dart
Setting PULS3_GIT_SHA=b88cdaf
✓ Upload successful.        (8.2s)
✓ Cloud build successful.   (142.1s)
✓ Rollout successful. 🚀    (52.0s)

$ api=https://puls3-hub-on-stellar.api.serverpod.space
$ curl -s -X POST -d '{}' $api/health/check
{"__className__":"BackendHealth","version":"1.0.0+b88cdaf"}
https://puls3-4lw.pages.dev -> 200
https://evil.example.com    -> 403
http://localhost:3000       -> 403

$ scloud log
INFO | Allowed browser origins: [https://puls3-4lw.pages.dev] (loopback not allowed, run mode production)
INFO | applyMigrations: true
INFO | Applied database migration:

$ scloud deploy --wet-run --show-files
│  │  ├─ passwords.yaml     (ignored)
│  ├─ .env                  (ignored)

$ cd puls3_server && dart analyze --fatal-infos && dart test test/unit
No issues found!
00:40 +285: All tests passed!

Notes for reviewers

  • The Cloud load balancer answers 411 to a POST without Content-Length, so every verify curl in the README sends -d '{}'.
  • Cloud variables set: SERVERPOD_APPLY_MIGRATIONS=true and PULS3_ALLOWED_ORIGINS=https://puls3-4lw.pages.dev. PULS3_GIT_SHA is set by the pre-deploy script. PULS3_STELLAR_* use the testnet defaults.
  • After chore(infra): testnet configuration and secrets management #106 merges, PULS3_ALLOWED_ORIGINS and PULS3_GIT_SHA should be added to .env.example and docs/infra/secrets.md (follow-up).
  • On Windows Git Bash, scloud must be run as scloud.bat or through cmd //c "scloud ..."; the README notes this.

@TOMOKI977 TOMOKI977 added area: infra CI/CD, deploy, testnet, secrets type: chore Maintenance and tooling P0 Blocks a deadline deliverable labels Oct 7, 2026
@TOMOKI977 TOMOKI977 self-assigned this Oct 7, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Deploying puls3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2bf134c
Status: ✅  Deploy successful!
Preview URL: https://f16545f0.puls3-4lw.pages.dev
Branch Preview URL: https://chore-31-serverpod-cloud-dep.puls3-4lw.pages.dev

View logs

@TOMOKI977
TOMOKI977 force-pushed the chore/31-serverpod-cloud-deploy branch from fa08b9b to 011090b Compare October 7, 2026 20:17
@TOMOKI977

Copy link
Copy Markdown
Contributor Author

Added in 011090b: when the Pages project sets PULS3_API_URL, scripts/cloudflare-pages-build.sh passes it as --dart-define. PULS3_API_URL=https://puls3-hub-on-stellar.api.serverpod.space/ is now set for both Pages environments (preview and production). The app reads that define once #106 merges; until then the build ignores it.

@TOMOKI977
TOMOKI977 force-pushed the chore/31-serverpod-cloud-deploy branch from 011090b to c2c1cb4 Compare October 7, 2026 20:27
@TOMOKI977
TOMOKI977 force-pushed the chore/31-serverpod-cloud-deploy branch from c2c1cb4 to d6f09b5 Compare October 7, 2026 21:59
health.check returns the package version, plus +<sha> when PULS3_GIT_SHA
is set, so a deployed server names the commit it runs. A unit test keeps
the version constant equal to pubspec.yaml, and an invalid value stops the
server at startup.
Serverpod 3.4.13 sends a static wildcard Access-Control-Allow-Origin and
answers preflights in its core middleware, before added middleware runs.
An origin gate on the API server now rejects requests whose Origin is not
loopback or listed in PULS3_ALLOWED_ORIGINS with 403 before any endpoint
runs, and echoes allowed origins with Vary: Origin. Requests without an
Origin header (curl, probes) pass through.
scloud.yaml uses the format serverpod_cloud_cli 1.0.0 writes, with a
placeholder project id that scloud project link replaces, and no
pre-deploy scripts: the Flutter web app ships to Cloudflare Pages and the
generated code is committed. The root .scloudignore keeps passwords, run-mode
configs, tests, Docker files and web/app out of the upload; paths are
workspace-relative because scloud reads it from the workspace root.
Document the first deploy (scloud install, login, project create and link,
variables, platform-managed passwords), the mapping of each secret in
docs/infra/secrets.md (#30) to Serverpod Cloud, the redeploy flow with the
health check, the PULS3_GIT_SHA version and the PULS3_ALLOWED_ORIGINS allow-list.
The tracker stays off in this deployment.
Loopback origins (localhost, 127.0.0.1, [::1]) are now allowed only in the
development run mode; production, staging and test allow just the origins
listed in PULS3_ALLOWED_ORIGINS. A request with several Origin headers is
rejected. server.dart builds the gate through originGateFromEnvironment,
which is tested end to end over HTTP, and new cases cover default-port and
duplicate normalization, Vary merging, an existing
Access-Control-Allow-Origin and lookalike loopback hosts.
scloud 1.0.0 runs scripts.pre_deploy on the deploying machine, in the server
directory, through cmd /c on Windows and bash -c elsewhere, before it zips
the project, and stops the deploy on a non-zero exit. The new
tool/set_deploy_sha.dart sets PULS3_GIT_SHA to git rev-parse --short HEAD
(suffixed -dirty when tracked files changed) with scloud variable set.
scloud.yaml now runs it and explains the project id placeholder.
First deploy is now seven ordered steps without jumps: a pinned CLI install
(1.0.1 on Dart 3.12.2+, or 1.0.0 with serverpod_cloud_shared pinned on older
Dart), login, project create with an explicit plan, link, variables, deploy
with the PULS3_GIT_SHA pre-deploy script, and verification including CORS
checks (200 for the Pages origin, 403 for another). Adds a failure section
(build log, logs, deployment status, fix forward or redeploy a good commit)
that marks the Cloud behaviour the CLI cannot confirm, and documents that
loopback origins are allowed in development only.
Loopback origins are allowed only in the development run mode, and Serverpod Cloud's run mode is not verified yet. A localhost-origin call that returns 200 after deploy reveals a non-production run mode.
…ealth checks

The Cloud load balancer answers 411 to a POST without Content-Length, so
the verify commands now send an empty JSON body.

Refs #31
…3_API_URL

When the Pages project sets PULS3_API_URL, the build passes it as a
dart-define, so the web app calls the Serverpod Cloud server.

Refs #31
@TOMOKI977
TOMOKI977 force-pushed the chore/31-serverpod-cloud-deploy branch from d6f09b5 to 2bf134c Compare October 7, 2026 22:51

@moises-cisneros moises-cisneros left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally on Flutter 3.44.4 (Dart 3.12.2, same as CI): dart analyze --fatal-infos is clean, 325 unit tests pass, and scripts/tests/cloudflare-pages-build.test.sh passes 7/7. I also probed the live API: /health/check returns 1.0.0+b88cdaf, the Pages origin gets 200, evil.example.com and localhost:3000 get 403.

Before merging

  • #114 is already merged, but this PR still targets chore/110-serverpod-4-upgrade. Please retarget it to main so the full CI runs.

Minor observations (non-blocking)

  1. set_deploy_sha.dart uses --untracked-files=no, so an untracked file that is not in .scloudignore is uploaded while the health version still reports a clean SHA.
  2. PULS3_GIT_SHA is set before the upload. If the upload or build fails, the variable already holds the new SHA while the old build keeps running.
  3. An OPTIONS preflight from a disallowed origin gets access-control-allow-origin: * from Serverpod core. The real request is still rejected with 403, so it is not exploitable, but the header is misleading. The doc comment on originGate already explains why.

The open acceptance criteria (app URL, Pages deploy, redeploy by a teammate) are acknowledged in the description.

@moises-cisneros
moises-cisneros merged commit fc8b9e6 into chore/110-serverpod-4-upgrade Oct 8, 2026
1 check passed
TOMOKI977 added a commit that referenced this pull request Oct 8, 2026
#117)

* build(serverpod)!: upgrade runtime and generated APIs to 4.0.4

* fix(migrations): preserve auth rate-limit rows in Serverpod 4 upgrade

Copy serverpod_auth_idp_rate_limited_request_attempt rows through a
transaction-scoped temp table while the generator recreates the table to
rename nonce to key. Document the upgrade, migration and rollback steps.

Refs #110

* build(server): bundle Serverpod 4 server and keep local secrets out of the image

Serverpod 4 dependencies use Dart build hooks, so the image now uses
dart build cli and ships the emitted bundle. The new .dockerignore keeps
.env files and config/passwords.yaml out of the build context, which
previously baked local secrets into the runtime image.

Refs #110

* ci: pin Flutter 3.44.4 / Serverpod 4.0.4 and build the server image

Add a docker job, triggered by server, domain, lockfile and
.dockerignore changes, that builds the image with planted secret files
and asserts the bundle exists and passwords.yaml is excluded.

Refs #110

* docs: update toolchain pins to Flutter 3.44.4 and Serverpod 4.0.4

Refs #110

* build(flutter): pin the Cloudflare Pages Flutter version in the repository

The Pages build command cloned Flutter 3.41.4 from the dashboard, so every
branch on Serverpod 4 (Dart ^3.12.2) failed version solving. The build now
runs scripts/cloudflare-pages-build.sh, which installs the branch's pinned
Flutter. A test keeps its version in sync with CI.

Refs #110

* docs(env): list PULS3_FLUTTER_DIR in .env.example

Refs #110

* fix(migrations): recreate the Serverpod 4 upgrade migration on top of the hire tables

The forced upgrade-4-0 migration was created before #93 added migration
20261006210458593 with the hire and hire_payment tables, so its snapshot
did not know about them. Recreate it with serverpod_cli 4.0.4 as
20261007214534790-upgrade-4-0 so it now sorts after 20261006210458593 and its
definition includes hire, hire_payment, chain_submission and the Serverpod 4
module tables. The migration does not create, drop or alter the hire tables.

The hand edit is re-applied: rows of
serverpod_auth_idp_rate_limited_request_attempt are copied to a temporary
table before Serverpod drops and recreates it, with nonce mapped to key, and
restored afterwards, so rate-limit history still survives the upgrade.

Refs #110

* docs(env): list PULS3_HIRE_JOB_DURATION_SECONDS in .env.example

#93 reads it in HireService without listing it, which fails
scripts/tests/env-inventory.test.sh on main.

Refs #110

* ci: run the offline script tests on every change

scripts/tests/*.test.sh were never run in CI, so main broke the env
inventory (PULS3_HIRE_JOB_DURATION_SECONDS) without a red check. The new
scripts job runs them all and gates the merge.

Refs #110

* feat(server): deploy the API to Serverpod Cloud with health version and CORS gate (#115)

* feat(server): report the deployed commit in the health version

health.check returns the package version, plus +<sha> when PULS3_GIT_SHA
is set, so a deployed server names the commit it runs. A unit test keeps
the version constant equal to pubspec.yaml, and an invalid value stops the
server at startup.

* feat(server): restrict browser callers to allowed origins

Serverpod 3.4.13 sends a static wildcard Access-Control-Allow-Origin and
answers preflights in its core middleware, before added middleware runs.
An origin gate on the API server now rejects requests whose Origin is not
loopback or listed in PULS3_ALLOWED_ORIGINS with 403 before any endpoint
runs, and echoes allowed origins with Vary: Origin. Requests without an
Origin header (curl, probes) pass through.

* chore(server): add Serverpod Cloud project config

scloud.yaml uses the format serverpod_cloud_cli 1.0.0 writes, with a
placeholder project id that scloud project link replaces, and no
pre-deploy scripts: the Flutter web app ships to Cloudflare Pages and the
generated code is committed. The root .scloudignore keeps passwords, run-mode
configs, tests, Docker files and web/app out of the upload; paths are
workspace-relative because scloud reads it from the workspace root.

* docs: add Serverpod Cloud deploy and redeploy steps

Document the first deploy (scloud install, login, project create and link,
variables, platform-managed passwords), the mapping of each secret in
docs/infra/secrets.md (#30) to Serverpod Cloud, the redeploy flow with the
health check, the PULS3_GIT_SHA version and the PULS3_ALLOWED_ORIGINS allow-list.
The tracker stays off in this deployment.

* fix(server): allow loopback origins only in development

Loopback origins (localhost, 127.0.0.1, [::1]) are now allowed only in the
development run mode; production, staging and test allow just the origins
listed in PULS3_ALLOWED_ORIGINS. A request with several Origin headers is
rejected. server.dart builds the gate through originGateFromEnvironment,
which is tested end to end over HTTP, and new cases cover default-port and
duplicate normalization, Vary merging, an existing
Access-Control-Allow-Origin and lookalike loopback hosts.

* feat(server): set PULS3_GIT_SHA in a Serverpod Cloud pre-deploy script

scloud 1.0.0 runs scripts.pre_deploy on the deploying machine, in the server
directory, through cmd /c on Windows and bash -c elsewhere, before it zips
the project, and stops the deploy on a non-zero exit. The new
tool/set_deploy_sha.dart sets PULS3_GIT_SHA to git rev-parse --short HEAD
(suffixed -dirty when tracked files changed) with scloud variable set.
scloud.yaml now runs it and explains the project id placeholder.

* docs(server): make the Serverpod Cloud first deploy one linear path

First deploy is now seven ordered steps without jumps: a pinned CLI install
(1.0.1 on Dart 3.12.2+, or 1.0.0 with serverpod_cloud_shared pinned on older
Dart), login, project create with an explicit plan, link, variables, deploy
with the PULS3_GIT_SHA pre-deploy script, and verification including CORS
checks (200 for the Pages origin, 403 for another). Adds a failure section
(build log, logs, deployment status, fix forward or redeploy a good commit)
that marks the Cloud behaviour the CLI cannot confirm, and documents that
loopback origins are allowed in development only.

* chore(server): recommend --from-file for Serverpod Cloud passwords

* docs(server): check that the deployed API rejects localhost origins

Loopback origins are allowed only in the development run mode, and Serverpod Cloud's run mode is not verified yet. A localhost-origin call that returns 200 after deploy reveals a non-production run mode.

* chore(server): link Serverpod Cloud project puls3-hub-on-stellar

Refs #31

* docs(server): record the Serverpod Cloud API URL and send a body in health checks

The Cloud load balancer answers 411 to a POST without Content-Length, so
the verify commands now send an empty JSON body.

Refs #31

* feat(flutter): point the Pages build at the deployed API through PULS3_API_URL

When the Pages project sets PULS3_API_URL, the build passes it as a
dart-define, so the web app calls the Serverpod Cloud server.

Refs #31

* docs(env): list PULS3_ALLOWED_ORIGINS and PULS3_GIT_SHA in .env.example

Refs #31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: infra CI/CD, deploy, testnet, secrets P0 Blocks a deadline deliverable type: chore Maintenance and tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants