Public endpoints and domains
A service can be reached in two ways: privately, from inside your project, or publicly, from the internet. This page shows you the URL each one gives you and how the encryption is handled. It also states plainly what does not exist yet. There is no access control on a published service, and there are no custom domains.
Private by default
A new service is internal: reachable only from inside the platform, not from the internet. Every service always has an internal URL of this shape:
http://<private-hostname>
That address only resolves inside the platform's own network, so typing it into your browser does nothing. Other workloads in your project — agents, functions, other services — call it directly. It appears as internal_url on the service object and in the console's URL block.
"Internal" removes your service from the internet. It does not check who is calling. Any workload that can reach the platform's shared gateway can reach an internal service by addressing it by name. If your service needs to know its callers, add authentication in your own application code.
Publish to the internet
Turn on publish and the platform gives your service a public HTTPS URL:
https://<name>-<project-short>.apps.codyhill.dev
The platform builds that hostname for you. It joins two parts with a dash: your service name and your project short id. Both sit under the shared apps.codyhill.dev domain. So a service named checkout-api in project acme becomes https://checkout-api-acme.apps.codyhill.dev. You cannot choose a different hostname with this flag — see Custom domains below.
Publish is a single boolean, settable at create time or later:
- platformctl
- curl
- Console
platformctl serverless update checkout-api --publish
Publishing is replaced as a pair, enabled and host together, so --publish-host refuses to travel without --publish rather than taking your service off the internet as a side effect. The platform ignores host anyway — see Custom domains.
curl -sS -X PATCH "$CAI_SERVERLESS_API/v1/projects/$CAI_PROJECT/services/checkout-api" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'Content-Type: application/json' \
-d '{"publish": {"enabled": true}}'
Check Publish to the internet — under Advanced options in the deploy dialog, or on the Edit form for a service that already exists. The service list shows a visibility badge reading published or Internal. While a change is still rolling out, a second badge appears next to it reading converging, and the service's page says plainly that the live state and the state you asked for do not match yet.
Publishing does not finish while you wait for the response. Ask the platform for the service every few seconds and watch for the external URL to appear:
- platformctl
- curl
- Console
platformctl serverless get checkout-api
You should see, once publishing completes:
NAME STATE PUBLISHED URL
checkout-api ready yes https://checkout-api-acme.apps.codyhill.dev
The URL column prefers external_url and falls back to the internal address, so the row changing to an https:// address is itself the signal that publishing finished. Add -o json for every field.
curl -sS -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_SERVERLESS_API/v1/projects/$CAI_PROJECT/services/checkout-api" \
| jq '.state, .published, .external_url'
You should see, once publishing completes:
"ready"
true
"https://checkout-api-acme.apps.codyhill.dev"
The service's Overview tab carries a URL block. Public HTTPS URL appears there, with a Copy button, once the address is genuinely reachable. Until then you see the gateway address and a line saying the real URL is on its way.
Key operational details:
external_urlis filled in only once the URL actually works. The platform will not report a public address until it has checked that the hostname answers over valid TLS. Until then the field is empty.- The
Exposedcondition holds readiness back. For a published service,statedoes not readready— andreadystaysfalse— while the public URL is still being wired up. The service'smessagethen carries theExposedcondition's own words naming the step it is waiting on — see troubleshooting.
To take a service off the internet again, run platformctl serverless update checkout-api --publish=false, PATCH {"publish": {"enabled": false}}, or clear the checkbox in Edit. Deleting a service releases its public hostname.
Published means open to everyone
Publishing puts your service on the public internet with no gate in front of it. Anyone who knows the URL can call it, from anywhere. The platform does not check identities, tokens, signatures, or source IP addresses on the way in.
This surprises people who come from other clouds. publish has exactly two settings: enabled and host. The platform ignores host. There is no third field and no separate API for any of the following:
| What you may be looking for | Status on this platform |
|---|---|
An "invoker" role, so only certain identities may call the service (like GCP Cloud Run's roles/run.invoker) | Does not exist |
Requiring a signed or authenticated request by default (like AWS Lambda function URLs' AWS_IAM auth type) | Does not exist |
| A source-IP allowlist, a firewall rule, or a web application firewall that inspects traffic before it reaches you | Does not exist |
| An API key or shared secret checked by the platform | Does not exist |
The same is true of a published function. Its public URL is open too, which is why the function quickstart calls one with a plain curl and no credential.
What to do instead
- Publish it through the API Gateway instead of this flag. That is a separate path to the internet, and it is the one with the controls this flag lacks: a required credential (a token from your identity provider, or a platform-issued API key), an address allow list, a rate limit, and your own domain. Leave
publish.enabledoff and create a gateway endpoint pointing at the service — see Publish an endpoint. Everything below still applies to this flag itself. - Keep it internal if it does not need to be public. An internal service is off the internet. Callers inside the platform are still not checked, but nobody outside can reach it at all. This is the right answer for admin tools, back-office jobs, and anything a browser never touches directly.
- Authenticate in your own application code. On every request, check an
Authorizationheader, a shared secret, or a signed request inside your handler. Store the secret with Secrets Manager and read it from the environment. Do not hardcode it. This is the only real gate available for a published service today. - Assume the URL is public knowledge. The hostname comes from your service name and project short ID, so anyone can guess it. A secret URL is not a security control.
- For an agent chat widget specifically, the embeddable chat path has a control the serverless path lacks. It uses an embed key plus a list of web page addresses allowed to load the widget. The server checks both. That covers embedding an agent in a web page, not calling a serverless service.
Ensure appropriate authentication controls are implemented on published public endpoints. See Security overview for details.
How TLS is handled
TLS is the encryption behind https:// and the browser padlock, and the platform handles all of it. One shared wildcard certificate covers every *.apps.codyhill.dev hostname. There is nothing for you to request, upload, or renew. There is also no option to bring your own certificate today.
Test a revision directly with tags
When you split traffic between revisions, you can put a tag on a traffic target. A tag is a short label such as canary. A tagged revision gets its own hostname, so you can send test requests straight to the new revision without touching the percentage split. That URL appears in the url field of the target inside active_traffic — the split actually being served, as distinct from the traffic you requested — and on the console's Revisions tab. See deploy a service for the traffic-split API.
Custom domains
Not through this flag. With publish.enabled, your public URL is always https://<name>-<project-short>.apps.codyhill.dev. One sharp edge worth naming: the API accepts and stores a publish.host field and the platform ignores it, so do not set it expecting a custom hostname.
To serve on a domain you own, publish the service through the API Gateway instead. You add the domain to your project, create one CNAME record, and publish an endpoint on it; the certificate is handled for you. Subdomains only — api.example.com, not example.com and not *.example.com.
Summary
| Setting | Value |
|---|---|
| Default visibility | Internal (publish.enabled: false) |
| Internal URL | http://<private-hostname> — always present, plain HTTP, unauthenticated |
| Public URL | https://<name>-<project-short>.apps.codyhill.dev — appears in external_url only once live |
| Caller authentication | None, published or internal. No invoker role, no signed-request mode, no IP allowlist — authenticate in your own code |
| Hostname choice | None — always computed from service name + project short ID; publish.host is ignored |
| TLS | Automatic, shared wildcard certificate; HTTPS on published services only |
| Custom domains | Configurable via platform Gateway |
| Readiness | Exposed condition keeps ready at false until the public URL answers over valid TLS |
Next steps
- Troubleshooting serverless — what to do when publishing sticks.
- Serverless API reference — every field shown on this page.