Surfaces and Wiring
A surface is a transport medium through which entrypoints are reached. The wiring section of your spec maps each surface's native concepts to your typed entrypoints.
Why this exists
Without a formal surface model, business logic leaks into transport handlers. Each surface re-implements its own input binding, access enforcement, and error translation. Adding a new surface requires duplicating handler code.
In Gooi, your domain, queries, and mutations know nothing about surfaces. The wiring section is the only place where surfaces exist. Adding a new surface requires adding to wiring and nothing else.
The four runtime surfaces
Gooi supports four runtime surfaces. Each surface delivers invocations to runtime.invoke() by mapping its native concepts to typed entrypoint inputs.
| Surface | What it maps | Typical use |
|---|---|---|
http | HTTP method + path + query/body params | REST APIs |
web | Browser events and state paths | Browser applications |
cli | Command paths and flags | Developer tooling |
webhook | HTTP POST payload fields | Event receivers |
Wiring an HTTP surface
wiring:
surfaces:
http:
queries:
list_messages:
method: GET
path: /messages
bind:
q: query.q
sort_by: query.sort_by
sort_order: query.sort_order
page: query.page
page_size: query.page_size
mutations:
submit_message:
method: POST
path: /messages
bind:
message: body.message
delete_message:
method: DELETE
path: /messages/:id
bind:
id: path.id
The bind block maps transport buckets (query.*, body.*, path.*) to typed entrypoint input fields. This is input binding only. Access enforcement and business logic are not declared here.
Wiring a CLI surface
wiring:
surfaces:
cli:
queries:
list_messages:
command:
path: messages list
bind:
q: args.query
sort_by: flags.sort-by
page: flags.page
list_messages_with_authors:
command:
path: messages list
when:
flags:
enriched: true
bind:
q: args.query
mutations:
submit_message:
command:
path: messages send
bind:
message: args.message
CLI commands use explicit path and optional when.flags conditions to distinguish overlapping command shapes. Avoid sharing a command path across two queries without a when condition.
Wiring a web surface
wiring:
surfaces:
web:
routes:
view_home:
bind:
search_query: query.q
sort_by: query.sort_by
page: query.page
mutations:
submit_message:
bind:
message: body.message
Web surface bindings map URL query parameters and request body fields to entrypoint inputs. View nodes reference queries by name for data binding; that binding is declared in views, not here.
What wiring cannot do
These constraints are intentional.
- Wiring cannot reference providers by name. Supabase, Redis, S3: none of these appear in
wiring. Provider selection is a deploy-time concern committed to the lockfile. - Wiring cannot implement business logic. The
bindblock maps transport fields to entrypoint inputs. It does not filter, transform, or compute values. - Wiring cannot override access rules. Access is declared on each entrypoint in
queriesandmutations. The wiring section has no authority over access.
A spec with provider names or resource identifiers inside wiring is malformed. The compiler emits a DEPLOY_CONCERN_IN_SPEC error for this.
What's next
- Queries and Mutations: the entrypoints that wiring maps to surfaces
- Ecosystem: how providers are resolved and bound at deploy time