Same-Origin Proxy
Browser ──/cms/*──▶ Your Astro server ──server-side request──▶ Kenresoft API WorkerThe proxy is an API and authentication gateway for a frontend. It is not the CMS admin UI, and it does not expose the admin API.
Why it exists
Section titled “Why it exists”By default the API’s session cookie belongs to the API’s own origin. Your website’s server never sees it, so no server-side “is this visitor signed in?” checks, and browsers that block third-party cookies drop it entirely. With the proxy, the browser talks only to your site (/cms/*), your server forwards to the API, and the API’s Set-Cookie lands on your site’s own host. Both the browser and SSR then see the cookie, on any domain layout.
Without the proxy (“direct mode”), sign-in was measured working in Chromium and failing in WebKit and in Firefox with third-party cookies blocked. Use the proxy for any site with accounts.
The create astro command and the new-site starter both create it at src/pages/cms/[...path].ts. The generated file for Cloudflare:
import type { APIRoute } from 'astro';import { env } from 'cloudflare:workers';import { createCmsProxy } from '@kenresoft-cms/astro';
export const prerender = false;
// The Worker's runtime variable first, then the value from the build.const cmsUrl = () => (typeof env.PUBLIC_KENRESOFT_CMS_URL === 'string' && env.PUBLIC_KENRESOFT_CMS_URL) || import.meta.env.PUBLIC_KENRESOFT_CMS_URL;
export const ALL: APIRoute = ({ request }) => createCmsProxy({ url: cmsUrl(), trustedProxySecret: typeof env.TRUSTED_PROXY_SECRET === 'string' ? env.TRUSTED_PROXY_SECRET : undefined, })(request);The Node version reads process.env the same way. Reading the URL at runtime first means a build made without PUBLIC_KENRESOFT_CMS_URL (for example a Cloudflare Git build without the Build variable) still works when the deployed Worker has the variable. Sites created before @kenresoft-cms/create 0.3.3 read it at build time only; run --update to get this version.
If the proxy has no URL at all, it answers every request with 503 ({"error":"The CMS connection is not configured"}) and logs which variable is missing.
| Option | Default | Purpose |
|---|---|---|
url |
(required) | The CMS API’s real base URL |
basePath |
/cms |
The path prefix the proxy is mounted at |
trustedProxySecret |
none | Must equal the API’s TRUSTED_PROXY_SECRET |
fetch |
global fetch |
Override, for testing |
The proxy needs on-demand (server) rendering and a deployment adapter.
What the proxy forwards
Section titled “What the proxy forwards”Only these upstream paths are allowed. Anything else, including /api/v1/admin/* and the system routes, returns 404 without contacting the API:
/api/v1/auth/*/api/v1/public/*/api/v1/account/*(the signed-in account’s own form submissions;@kenresoft-cms/astro0.8.0 and later)/api/plugins/<id>/public/*
A request outside the base path also returns 404. Path traversal is collapsed before the check, so .. cannot escape the allow-list.
- Trailing slash: a trailing slash is dropped before forwarding, because the API rejects paths that end in one. This matters on sites with Astro’s
trailingSlash: 'always'(fixed in@kenresoft-cms/astro0.6.1; older versions fail every call). See Troubleshooting. - Body: forwarded byte for byte for every method except
GETandHEAD, somultipart/form-datauploads keep their boundary and files intact. - Request headers forwarded:
cookie,content-type,accept,accept-language,idempotency-key,origin,user-agent,x-turnstile-token. Nothing else is passed on. - Response: status, body and all headers pass through, including every
Set-Cookie, exceptcontent-encoding,content-length,transfer-encodingandconnection, which describe bytesfetchalready decoded. - Redirects (for example the email-verification link’s callback) are returned to the browser untouched, not followed.
Cookies and CORS
Section titled “Cookies and CORS”- The cookie is set by the API with
SameSite=None; Secure, and passes to the browser under your site’s host. - The proxy forwards the browser’s
Originheader on sign-in and form requests, and Better Auth checks it. Your site’s origin must therefore be in the API’sCORS_ORIGINS, even though the browser only calls your own domain. Plain server-side content reads send noOrigin, so a site can render pages and still fail its first sign-in. See Authentication and CORS. BETTER_AUTH_URLmust stay the API’s own URL, not your site’s.
Using it from code
Section titled “Using it from code”// Browser code: talk to your own originconst browserCms = createKenresoftClient({ url: '/cms' });
// Server (SSR) code: call the API directly, forwarding the first-party cookieconst cmsForRequest = createKenresoftClient({ url: import.meta.env.PUBLIC_KENRESOFT_CMS_URL, cookies: Astro.request.headers.get('cookie'),});The managed src/lib/kenresoft.ts exports cms, cmsForRequest(request) and browserCms. Call the state-changing auth methods from browser code (a <script> or client island), since the cookie must land in the visitor’s own cookie jar.
Preserve visitor IPs for rate limiting
Section titled “Preserve visitor IPs for rate limiting”Behind the proxy, the API sees your Astro Worker as the client, so all visitors share one rate-limit bucket. To keep per-visitor limits accurate, set the same secret on both sides. The secret is a random string you generate yourself. It is not issued by Cloudflare or Kenresoft.
-
Generate it:
Terminal window node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -
Set it as a Worker secret in the CMS project, then in the Astro site project (run the command in each project directory and paste the same value):
Terminal window wrangler secret put TRUSTED_PROXY_SECRET -
Confirm both Workers have it with
wrangler secret list.
The proxy sends x-kenresoft-proxy-secret and x-kenresoft-client-ip (taken from cf-connecting-ip, or the first x-forwarded-for entry) when it has both a secret and a client IP. The API trusts the forwarded IP only if the secret matches, using a constant-time comparison. Unset on the API, or a mismatch, and the headers are ignored and everyone shares one limit, with no error.
The generated proxy reads the secret from the runtime environment: the Worker env on Cloudflare, process.env on Node. No code change is needed. Never expose it through a PUBLIC_ variable and never commit it. For local development, put it in .dev.vars (Cloudflare) or your shell environment, and keep that file gitignored. See Rate limiting.
Cloudflare Worker to Worker
Section titled “Cloudflare Worker to Worker”If your site is a Cloudflare Worker calling a CMS API Worker in the same account, add global_fetch_strictly_public to your Wrangler compatibility_flags. Without it, the call fails with error code: 1042, which surfaces as a 404. This flag is only needed on Cloudflare; a site on another host does not need it. See Astro deployment.
Direct mode
Section titled “Direct mode”The browser can call the API directly (Browser → CMS API). It is simpler but browser-only, depends on cross-site cookies, and gives server-rendered pages no session. Add your origin to CORS_ORIGINS and call the mutating auth methods from the browser.