App routes
Declare routes in a schema-3 manifest to give a frontend file another URL or invoke a Function at an app URL. The selected deployment supplies the route table and targets together. Routes work on both the OpenCloud address and a connected custom domain.
The pinned public CLI 3.9.0 supports routes in local validation, bundling, development, and deployment commands. Function routes require SDK 2.3.0.
Alias files and invoke a Function
schemaVersion: 3
appId: 6f9619ff-8b86-4e6e-a62a-889950f42d3e
frontend:
directory: frontend
spa: true
runtime:
sdk:
version: 2.3.0
functions:
- name: pixel
entrypoint: functions/pixel/index.ts
access: public
routes:
- id: favicon
path: /favicon.png
asset: icons/app.png
access: public
- id: apple-icon
path: /apple-icon.png
asset: icons/app.png
access: public
- id: pixel
path: /pixel.jpg
function: pixel
methods: [GET, HEAD]
health:
path: /Both icon URLs serve the exact frontend/icons/app.png file. An asset target is a regular file relative to frontend.directory; it cannot escape that directory, select a directory, interpolate parameters, or redirect to another origin. Missing targets fail validation. Asset routes support exactly GET and HEAD, which are their default methods.
/pixel.jpg?param1=test123 invokes pixel; the query does not select the route. A .jpg suffix does not choose a response format: the Function returns the bytes and content type, or a JSON result. Function routes require the paired SDK 2.3.0 and a declared user or public Function. They cannot invoke system Functions or match health.path.
Parameters
OpenCloud uses a bounded subset of modern path-to-regexp syntax:
| Pattern | Matches | Parameters |
|---|---|---|
/users/:id | /users/42 | id: "42" |
/users{/:id} | /users, /users/42 | id is absent or a string |
/docs{/index} | /docs, /docs/index | none |
/report{.:ext} | /report, /report.csv | ext is absent or "csv" |
/files/:id{.:ext} | /files/annual.report.csv | id: "annual.report", ext: "csv" |
/downloads/*parts | /downloads/a/b | parts: ["a", "b"] |
Required parameters occupy a whole path segment. Optional braces contain one whole literal or parameter segment, or a final {.:ext} extension as shown above. A named wildcard is terminal and consumes one or more segments; it does not match the empty suffix. Optional extensions split a parameter stem at the final dot. Omitted optional parameters have no key.
Use unique identifier names such as id, fileId, and parts. Each route allows up to 16 parameters, four non-nested optional groups, and one terminal wildcard. Limits are 100 routes, 512 characters per pattern, 4,096 UTF-8 bytes per request URI, and 100 ordered query pairs. Do not use legacy :id?, regex constraints, unnamed wildcards, nested groups, or required mixed segments such as /file-:id.
Matching is case-sensitive and treats a trailing slash as significant. Path segments are decoded once; malformed escapes, encoded separators, dot segments, and repeated interior slashes are rejected. Query values follow URL search-parameter decoding, including + as a space.
Specific literals take precedence over matching parameters, and parameters over wildcards. An explicit path wins over the equivalent optional expansion. Unresolved overlaps fail validation, including two equivalent paths with different parameter names or different method lists. Declaration order does not resolve conflicts. Put all supported methods for one path on one route.
Routing chooses the most specific path before checking its methods. An unsupported method returns 405 with Allow; it does not fall through to a broader route, a physical file, or the SPA. Function methods are an explicit subset of GET, HEAD, POST, PUT, PATCH, and DELETE. HEAD is opt-in and executes the handler while suppressing response bytes; keep GET/HEAD handlers safe for retries and avoid write side effects.
Function HTTP context
SDK 2.3.0 adds http to the Function context. It is null for ordinary SDK, queue, cron, or other non-route invocations. A routed invocation receives read-only routeId, method, pathname, params, and query.
import { defineFunction, schema } from "@opencloud/server";
export default defineFunction({
input: schema.object({}),
handler: async ({ http }) => ({
route: http?.routeId ?? null,
campaign: http?.query.get("param1") ?? null,
tags: http?.query.getAll("tag") ?? [],
}),
});For /pixel.jpg?param1=test123&tag=one&tag=two, campaign is test123 and tags preserves both values. query.get(name) returns the first value or null; getAll(name) returns every value; has(name) checks presence; and entries() iterates ordered pairs. Query and path parameters do not merge into input. Validate their values in your Function before using them.
GET/HEAD receive {} as input. Other methods use the existing JSON-body input parser and schema. The context does not expose raw headers, cookies, credentials, a Request object, or arbitrary raw request bodies. Normal Function response and streaming behavior remains available.
Public icons on a private app
Private-app favicons do not have to be public. Use access: public only when an icon must load before sign-in. That exception is allowed solely on an exact literal asset route, as in the two icon aliases above. Other asset routes default to access: inherit; optional and wildcard asset routes cannot be public.
The exception applies to the alias URL only. It does not make the underlying file URL, directory, app HTML, SDK configuration, data, or Functions public. Route responses are no-store, including filenames that resemble content hashes. Function routes always inherit app admission and preserve the Function's declared access: access: public on a Function does not bypass a private app's login requirement.
Routes cannot replace /, /_opencloud, /auth/v1, /rest/v1, /realtime/v1, /storage/v1, /functions/v1, or /.well-known/acme-challenge and their descendants. These platform namespaces remain reserved even behind a wildcard. Unmatched ordinary paths retain the existing static-file and configured SPA fallback behavior.
Exercise aliases, repeated queries, optional parameters, denied methods, and private-app access in the isolated development loop before promotion. Adding routes does not rewrite older SDK artifacts or require a no-routes manifest migration.
