The scaffolded worker.ts already uses these for every cross-worker call. They prefer service bindings (set up automatically on deploy) and fall back to HTTPS URLs in local dev.
import {
apiWorkerFetch, platformWorkerFetch, authWorkerFetch,
resolveApiTransport,
createScopedR2Handler,
} from 'deepspace/worker'
import type {
ApiWorkerEnv, PlatformWorkerEnv, AuthWorkerEnv,
ScopedR2Config, ScopedR2Handler, ScopedR2Auth, ScopeContext, PrefixResult,
} from 'deepspace/worker'
Do not replace these with raw c.env.X.fetch(...). wrangler dev doesn’t surface service bindings cross-process for SDK apps - the binding is undefined locally and the raw fetch silently fails. The helpers paper over the dev/prod mismatch.
apiWorkerFetch(env, path, init?)
Fetch the api-worker (Stripe, integrations, profiles, OAuth, usage tracking).
function apiWorkerFetch(
env: ApiWorkerEnv,
path: string,
init?: RequestInit,
): Promise<Response>
interface ApiWorkerEnv {
API_WORKER?: Fetcher // service binding (preferred in prod)
API_WORKER_URL?: string // HTTPS fallback (used in dev)
}
Prefers the API_WORKER service binding; falls back to API_WORKER_URL. Throws an actionable Error if neither is configured.
Fetch the platform-worker (cross-app DOs, R2 file gateway, WebSocket multiplexing).
function platformWorkerFetch(
env: PlatformWorkerEnv,
pathOrRequest: string | Request,
init?: RequestInit,
): Promise<Response>
interface PlatformWorkerEnv {
PLATFORM_WORKER?: Fetcher
PLATFORM_WORKER_URL?: string
}
Accepts either a path string or a full Request, so you can hand off c.req.raw derivatives with method/headers/body intact:
app.get('/ws/:roomId', async (c) => {
const roomId = c.req.param('roomId')
if (/^(workspace|dir|conv):/.test(roomId)) {
return platformWorkerFetch(c.env, c.req.raw)
}
return wsRoute((env) => env.RECORD_ROOMS)(c)
})
wsRoute in the example above is not an SDK export - it’s a small helper defined locally in the scaffolded worker.ts (around line 345 of templates/starter/worker.ts). It owns the JWT verification + URL rewriting boilerplate for WebSocket routes. Apps are free to edit or replace it.
For PNG capture against an *.app.space / *.deep.space URL, prefer the higher-level captureScreenshot wrapper instead of calling platformWorkerFetch against /internal/screenshot yourself - it owns the HMAC headers and the null-on-failure contract.
authWorkerFetch(env, path, init?)
Fetch the auth-worker (Better Auth, JWT issuance, OAuth flows).
function authWorkerFetch(
env: AuthWorkerEnv,
path: string,
init?: RequestInit,
): Promise<Response>
interface AuthWorkerEnv {
/**
* HTTPS URL for the auth-worker. Optional on the type so apps can declare
* a partial env shape during local bring-up, but the function throws if it's
* unset at call time.
*/
AUTH_WORKER_URL?: string
}
URL-only by design - the auth-worker has no service binding, which keeps Set-Cookie headers verbatim over plain HTTPS. (Service bindings strip some headers in transit.) Throws if AUTH_WORKER_URL is missing.
resolveApiTransport(env)
Exported for the AI helper, which needs the URL form to rewrite an internal https://api-worker.internal placeholder before calling provider SDKs. Most apps don’t need this.
function resolveApiTransport(env: ApiWorkerEnv):
| { kind: 'binding'; fetcher: Fetcher }
| { kind: 'url'; baseUrl: string }
Production note
Cross-worker calls over plain *.workers.dev URLs return Cloudflare error 1042 in production. The service binding is the only working transport for deployed apps; the URL fallback is a dev-only convenience the CLI writes into .dev.vars.
If a deployed app needs apiWorkerFetch or platformWorkerFetch, the corresponding [[services]] binding must be in wrangler.toml:
[[services]]
binding = "API_WORKER"
service = "deepspace-api"
[[services]]
binding = "PLATFORM_WORKER"
service = "deepspace-platform"
R2 helpers
createScopedR2Handler(config)
Factory that builds a route handler for prefix-scoped R2 reads/writes. The scaffold uses this implicitly via the useR2Files client hook - most apps don’t call it directly.
function createScopedR2Handler(config: ScopedR2Config): ScopedR2Handler
interface ScopedR2Config {
/**
* Resolve the R2 key prefix for the given scope.
* Called with the `?scope=` query param value (default: 'self').
*/
resolvePrefix(scope: string, ctx: ScopeContext): PrefixResult
/** Require a non-null userId for upload and delete. Default: true. */
requireAuthForMutations?: boolean
}
interface ScopeContext {
userId: string | null
url: URL
}
type PrefixResult =
| { prefix: string; error?: undefined }
| { prefix?: undefined; error: string }
type ScopedR2Handler = (
request: Request,
url: URL,
bucket: R2Bucket,
auth: { userId: string | null },
) => Promise<Response>
Security guarantees: download/delete keys are validated against the resolved prefix, path traversal (..) is rejected at the entry point, and mutations require a non-null userId by default.
See also