diff --git a/.claude/skills/capability-matrix/SKILL.md b/.claude/skills/capability-matrix/SKILL.md new file mode 100644 index 0000000..c022191 --- /dev/null +++ b/.claude/skills/capability-matrix/SKILL.md @@ -0,0 +1,115 @@ +--- +name: capability-matrix +description: Helps maintain the Supabase SDK capability matrix in capabilities/*.yaml and specs/ — naming a new feature ID, picking or creating a group, checking for duplicate or semantically-overlapping capabilities, spotting naming drift within a group, suggesting when a spec file is warranted, and noting platform-specific behavior. Use whenever a capability YAML or spec file is being added or edited, before opening a PR that touches capabilities/, or when asked to review/audit the matrix, check for duplicates, or suggest groupings. This is an advisory pass, not a gate — it complements `npm run validate`, it doesn't replace it. +--- + +# Capability Matrix Maintenance + +This repo is the canonical registry of features across Supabase's client SDKs +(`capabilities/*.yaml`). The JSON Schema and `npm run validate` already catch +everything mechanical: malformed IDs, area/filename mismatches, exact +duplicate IDs, orphaned spec files. What they can't catch is judgment — +whether a new feature is *actually* new, whether its name reads naturally +next to its siblings, whether it's filed under the right group. That +judgment is what this skill provides. + +This is a local, advisory pass. There is no CI bot version of this — findings +are suggestions for the person editing the file to accept, adjust, or ignore. +Don't present anything as a hard requirement. + +## Step 0: run the mechanical checks first + +Before spending any judgment calls, run the deterministic validator so you're +not duplicating what it already guarantees: + +```bash +cd scripts/capability-matrix && npm run validate +``` + +This confirms schema conformance, `area` field matches the filename, IDs +follow the `..` pattern, no two features +share an exact ID, and every spec file maps to a real feature. If this +fails, fix that first — the checks below assume a structurally valid file. + +## Step 1: read for context, not just the diff + +Read the *whole* target area file (`capabilities/.yaml`), not just the +new/changed entry — group names and sibling features are the only baseline +for judging naming and grouping consistency. If the new feature could +plausibly overlap another area (e.g. something touching both `realtime` and +`database`, or `client` and `auth` session handling), skim that file too. + +## Step 2: semantic duplicates + +`npm run validate` only catches identical IDs. Read every feature's `name` + +`description` in the same area (and group, if cross-area overlap looks +possible) and ask: does this describe behavior another entry already +covers, just worded differently? Common patterns to watch for: + +- Same underlying API call described from two angles (e.g. a "set" feature + and a separate "update" feature that hit the same endpoint) +- A new entry that's actually a narrower case of an existing one (should it + be a note on the existing feature instead of a new ID?) +- Copy-pasted description with only the verb changed + +If you find a likely duplicate, name both IDs and describe the overlap — +don't assume which one should win; that's the author's call. + +## Step 3: naming consistency + +The schema enforces the `..` shape via regex, but not +whether the words chosen fit. Compare the new feature's `id`/`name` against +its siblings in the same `group`: + +- Verb choice — if the group already uses `create`/`delete`/`list` for + parallel operations, a new `add_x` or `remove_x` reads inconsistent. + (See `CONTRIBUTING.md`'s "Choosing a feature ID" section for the + verb-object convention.) +- Admin/scoped variants should be namespaced the way existing ones are + (`auth.admin.delete_user`, not `auth.delete_user_admin`). +- `name` (the human-readable title) should match the tone of sibling + entries in the same group — not suddenly more/less verbose or technical. + +## Step 4: grouping + +- Does the feature's `group` field point to a group that actually fits, or + is it forcing a fit into the nearest existing one? If several recent + features don't cleanly fit any group, say so and suggest a new group + entry under `groups:` at the top of the file. +- Conversely, flag a group that's accumulated features with little in + common — that's a sign it should split. +- A feature with no `group` at all is valid (it's optional) but worth a + second look — is that intentional, or was a fitting group just missed? + +## Step 5: spec suggestion + +Spec files (`specs///.md`) are optional, +but valuable when a feature has real behavioral complexity: multiple named +error conditions, branching behavior, side effects, or prerequisites. The +directory always mirrors the feature `id`'s own segments — e.g. +`auth.mfa.challenge` lives at `auth/mfa/challenge.md` — regardless of what +that feature's optional `group` field currently says; the two can diverge +when a feature has been regrouped for display without renaming its `id`. +If the new feature's `description` hints at real complexity and no spec +exists, suggest creating one from `specs/TEMPLATE.md`. Don't suggest a spec +for a simple getter/setter with an already-complete one-line description. + +## Step 6: platform-scope notes + +There is no schema field for "this only applies to mobile/web SDKs" — that +nuance (biometric auth, secure enclave storage, browser-only APIs like +`localStorage`) is expected to live in prose, not structured data. If a +feature's behavior is inherently platform-scoped, suggest a line either in +the feature's `description` or, if it has a spec, in the spec's `## Notes` +section. Point out *why* it matters: SDKs that don't apply can declare +`not_applicable` in their `sdk-compliance.yaml`, but only if the constraint +is documented somewhere a maintainer would see it. + +## Presenting findings + +Group findings by step, lead with the ones most likely to need a real +change (duplicates, then naming, then grouping, then spec/platform notes). +For each finding, name the specific IDs involved and explain the reasoning +in one or two sentences — enough for the author to judge it themselves. +Skip steps that have nothing to report; don't manufacture a finding to fill +out every section. If everything looks clean, say so briefly and move on. diff --git a/.github/workflows/validate-capabilities.yml b/.github/workflows/validate-capabilities.yml index 20bc67d..46a2993 100644 --- a/.github/workflows/validate-capabilities.yml +++ b/.github/workflows/validate-capabilities.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - "capabilities/**" + - "specs/**" - "schema/**" - "scripts/capability-matrix/**" - "scripts/dart_symbol_extractor/**" diff --git a/.gitignore b/.gitignore index dcdfc9f..cdd3b6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Claude Code internal state (worktree metadata, session files) -.claude/ +.claude/* +# ...except committed project skills +!.claude/skills/ # Generated site output site/ diff --git a/capabilities/auth.yaml b/capabilities/auth.yaml index 639accf..8adf9f3 100644 --- a/capabilities/auth.yaml +++ b/capabilities/auth.yaml @@ -21,6 +21,8 @@ groups: title: Admin — OAuth Clients - id: passkey_admin title: Admin — Passkeys + - id: mfa_admin + title: Admin — Multi-Factor Authentication features: - id: auth.admin.create_provider name: Admin Create Provider @@ -33,7 +35,7 @@ features: - id: auth.admin.delete_mfa_factor name: Admin Delete MFA Factor description: Delete a specific MFA factor from a user's account. Requires admin/service role. - group: admin + group: mfa_admin - id: auth.admin.delete_provider name: Admin Delete Provider description: Delete a custom provider. Requires admin/service role. @@ -61,7 +63,7 @@ features: - id: auth.admin.list_mfa_factors name: Admin List MFA Factors description: List all MFA factors enrolled for a specific user. Requires admin/service role. - group: admin + group: mfa_admin - id: auth.admin.list_providers name: Admin List Providers description: List all custom providers with optional type filter. Requires admin/service role. @@ -176,11 +178,11 @@ features: group: passkey_admin - id: auth.passkey.register_passkey name: Register Passkey - description: Register a new WebAuthn passkey for the currently authenticated user. + description: Register a new WebAuthn passkey for the currently authenticated user. The underlying credential API is platform-specific — browsers use the native WebAuthn API, while mobile/desktop SDKs rely on the platform's credential manager — so availability varies by SDK. group: passkey - id: auth.passkey.sign_in_with_passkey name: Sign In with Passkey - description: Authenticate using a WebAuthn passkey registered on the device. + description: Authenticate using a WebAuthn passkey registered on the device. The underlying credential API is platform-specific — browsers use the native WebAuthn API, while mobile/desktop SDKs rely on the platform's credential manager — so availability varies by SDK. group: passkey - id: auth.session.auto_refresh name: Auto Refresh Token @@ -234,9 +236,9 @@ features: name: Resend Confirmation description: Resend a signup confirmation or OTP email/SMS to a user. group: sign_in - - id: auth.sign_in.reset_password - name: Reset Password - description: Send a password-reset link to a user's email address. + - id: auth.sign_in.send_password_reset_email + name: Send Password Reset Email + description: Send a password-reset link to a user's email address. Does not itself change the password — the link carries a recovery token that completes the reset via a subsequent sign-in/update-user call. group: sign_in - id: auth.sign_in.sign_in_anonymously name: Sign In Anonymously diff --git a/capabilities/database.yaml b/capabilities/database.yaml index 66dcda1..1b97fd4 100644 --- a/capabilities/database.yaml +++ b/capabilities/database.yaml @@ -25,7 +25,7 @@ features: - id: database.mutate.select_after_mutation name: Select After Mutation description: Chain a column selection onto an insert, update, upsert, or delete to return the affected rows in the response. - group: mutate + group: using_modifiers - id: database.mutate.update name: Update Rows description: Update rows in a table or view that match a filter. diff --git a/capabilities/functions.yaml b/capabilities/functions.yaml index 0856e30..ed16728 100644 --- a/capabilities/functions.yaml +++ b/capabilities/functions.yaml @@ -5,6 +5,8 @@ description: Invoke Supabase Edge Functions from the client with support for str groups: - id: invocation title: Invocation + - id: request_configuration + title: Request Configuration features: - id: functions.invocation.invoke name: Invoke Function @@ -13,7 +15,7 @@ features: - id: functions.invocation.set_auth_token name: Set Auth Token description: Update the authorization token sent with all subsequent function invocations. - group: invocation + group: request_configuration - id: functions.invocation.method_override name: HTTP Method Override description: Invoke a function using a non-default HTTP method (GET, PUT, PATCH, DELETE) for RESTful Edge Function designs. @@ -25,12 +27,12 @@ features: - id: functions.invocation.region_selection name: Region Selection description: Route an invocation to a specific geographic deployment region set at client construction time or overridden per individual call. - group: invocation + group: request_configuration - id: functions.invocation.timeout name: Invocation Timeout description: Cancel a function invocation after a specified timeout; composable with a caller-supplied cancellation signal. - group: invocation + group: request_configuration - id: functions.invocation.request_cancellation name: Request Cancellation description: Cancel an in-flight function invocation at any point before a response is received. - group: invocation + group: request_configuration diff --git a/capabilities/realtime.yaml b/capabilities/realtime.yaml index e804a29..3bfb30f 100644 --- a/capabilities/realtime.yaml +++ b/capabilities/realtime.yaml @@ -22,9 +22,9 @@ features: name: Create or Reuse Channel description: Create a new channel or return an existing one for the given topic. group: client - - id: realtime.channel.send - name: Send Message - description: Send a message to other subscribers of the channel. + - id: realtime.channel.broadcast + name: Broadcast Message + description: Send a broadcast message to other subscribers of the channel over the websocket connection. group: channel - id: realtime.channel.subscribe name: Subscribe @@ -117,7 +117,7 @@ features: - id: realtime.presence.presence_key name: Custom Presence Key description: Set a stable identifier for the current client's presence entries so multiple connections from the same user share one logical key via the presence.key channel option. - group: presence + group: subscriptions - id: realtime.configuration.custom_websocket_transport name: Custom WebSocket Transport description: Inject a custom WebSocket constructor at client construction time for runtimes without a native global WebSocket, such as Node.js (<22), Cloudflare Workers, or Deno. diff --git a/capabilities/storage.yaml b/capabilities/storage.yaml index c85e6b0..6a1fbfc 100644 --- a/capabilities/storage.yaml +++ b/capabilities/storage.yaml @@ -70,11 +70,7 @@ features: group: file_buckets - id: storage.file_buckets.list_files name: List Files - description: List files and folders within a path of the bucket. - group: file_buckets - - id: storage.file_buckets.list_files_paginated - name: List Files (paginated) - description: List files and folders within a bucket with cursor-based pagination support. + description: List files and folders within a path of the bucket, with optional cursor-based pagination. group: file_buckets - id: storage.file_buckets.move name: Move File @@ -200,13 +196,41 @@ features: name: Delete Analytics Bucket description: Delete an empty analytics bucket. group: analytics - - id: storage.analytics.iceberg_namespace - name: Iceberg Namespace Management - description: Create, list, and drop namespaces within an analytics bucket via the Iceberg REST Catalog API. + - id: storage.analytics.create_namespace + name: Create Iceberg Namespace + description: Create a new namespace within an analytics bucket via the Iceberg REST Catalog API. + group: analytics + - id: storage.analytics.list_namespaces + name: List Iceberg Namespaces + description: List namespaces within an analytics bucket via the Iceberg REST Catalog API. + group: analytics + - id: storage.analytics.delete_namespace + name: Delete Iceberg Namespace + description: Drop a namespace from an analytics bucket via the Iceberg REST Catalog API. + group: analytics + - id: storage.analytics.create_table + name: Create Iceberg Table + description: Create a new Iceberg table within a namespace, including schema and partition spec definition. + group: analytics + - id: storage.analytics.list_tables + name: List Iceberg Tables + description: List Iceberg tables within a namespace. + group: analytics + - id: storage.analytics.load_table + name: Load Iceberg Table + description: Load metadata for a specific Iceberg table within a namespace. + group: analytics + - id: storage.analytics.update_table + name: Update Iceberg Table + description: Update an Iceberg table's schema or partition spec. + group: analytics + - id: storage.analytics.rename_table + name: Rename Iceberg Table + description: Rename an Iceberg table to a new identifier within the same namespace. group: analytics - - id: storage.analytics.iceberg_table - name: Iceberg Table Management - description: Create, list, load, update, rename, and drop Iceberg tables within a namespace, including schema and partition spec definition. + - id: storage.analytics.delete_table + name: Delete Iceberg Table + description: Drop an Iceberg table from a namespace. group: analytics - id: storage.errors.error_codes name: Service Error Codes diff --git a/specs/auth/mfa/enroll.md b/specs/auth/mfa/enroll.md new file mode 100644 index 0000000..d59a832 --- /dev/null +++ b/specs/auth/mfa/enroll.md @@ -0,0 +1,29 @@ +# MFA Enroll + +Register a new multi-factor authentication factor for the current user. The factor is created in an unverified state and must complete a [challenge and verify](auth.mfa.verify) before it counts toward the session's authenticator assurance level. + +## Behavior + +Accepts a factor type — `totp` or `phone` — and returns a newly created factor along with whatever provisioning data that type requires: + +- **TOTP:** the server returns the data needed to add the factor to an authenticator app (typically a QR-encodable URI and a plain-text secret for manual entry). No message is sent to the user; the next step is a challenge against this factor, verified with a code generated by the authenticator app. +- **Phone:** the server associates the factor with the supplied phone number. Depending on project configuration, enrollment may itself trigger the first challenge (an SMS/WhatsApp OTP), or a separate challenge call may be required — SDKs should not assume which without checking the response. + +The factor is not usable for authentication until a subsequent challenge is verified. A user may have multiple enrolled factors, including more than one of the same type. + +## Prerequisites + +The caller must have an active session. Enrollment is performed by an authenticated user on their own account — it is not part of the sign-in flow itself (see [Admin Delete MFA Factor](auth.admin.delete_mfa_factor) / [Admin List MFA Factors](auth.admin.list_mfa_factors) for the admin-side equivalents on another user's account). + +## Notes + +- An unverified factor left unverified may be cleaned up by the server after some time; SDKs should not treat enrollment alone as a terminal state. +- Exact rate limits and per-user factor caps are project-configurable and not part of this capability's contract. + +## Related + +- [MFA Challenge](auth.mfa.challenge) — required next step to activate the enrolled factor +- [MFA Verify](auth.mfa.verify) — completes the challenge +- [MFA Challenge and Verify](auth.mfa.challenge_and_verify) — combines the two in one call +- [MFA Unenroll](auth.mfa.unenroll) — removes a factor, verified or not +- [MFA List Factors](auth.mfa.list_factors) — lists factors including their verification state diff --git a/specs/auth/oauth_server/approve_authorization.md b/specs/auth/oauth_server/approve_authorization.md new file mode 100644 index 0000000..fee0d0b --- /dev/null +++ b/specs/auth/oauth_server/approve_authorization.md @@ -0,0 +1,21 @@ +# OAuth Approve Authorization + +Approve a pending OAuth authorization request, completing the consent step of Supabase acting as an OAuth server on behalf of the current (resource-owner) user. + +## Behavior + +Takes the identifier of a pending authorization request — obtained via [OAuth Get Authorization Details](auth.oauth_server.get_authorization_details) — and approves it on behalf of the currently authenticated user. Approval results in the requesting OAuth client receiving whatever grant artifact its flow specifies (e.g. an authorization code delivered via redirect), and creates a durable grant record the user can later review or revoke. + +Once approved, the same authorization request cannot be approved or denied again. + +## Prerequisites + +- The caller must have an active session — approval is performed as the resource owner, not the OAuth client. +- The authorization request must exist and be pending; use [OAuth Get Authorization Details](auth.oauth_server.get_authorization_details) to fetch and display it (client name, requested scopes) before approving. + +## Related + +- [OAuth Get Authorization Details](auth.oauth_server.get_authorization_details) — fetch the pending request before deciding +- [OAuth Deny Authorization](auth.oauth_server.deny_authorization) — the rejection counterpart +- [OAuth List Grants](auth.oauth_server.list_grants) — view grants created by past approvals +- [OAuth Revoke Grant](auth.oauth_server.revoke_grant) — revoke a grant after the fact diff --git a/specs/auth/oauth_server/deny_authorization.md b/specs/auth/oauth_server/deny_authorization.md new file mode 100644 index 0000000..1524ff2 --- /dev/null +++ b/specs/auth/oauth_server/deny_authorization.md @@ -0,0 +1,19 @@ +# OAuth Deny Authorization + +Deny a pending OAuth authorization request, rejecting the consent step of Supabase acting as an OAuth server on behalf of the current (resource-owner) user. + +## Behavior + +Takes the identifier of a pending authorization request — obtained via [OAuth Get Authorization Details](auth.oauth_server.get_authorization_details) — and denies it on behalf of the currently authenticated user. No grant is created and the requesting OAuth client receives an error response rather than an authorization code. + +Once denied, the same authorization request cannot be approved or denied again. + +## Prerequisites + +- The caller must have an active session — denial is performed as the resource owner, not the OAuth client. +- The authorization request must exist and be pending. + +## Related + +- [OAuth Get Authorization Details](auth.oauth_server.get_authorization_details) — fetch the pending request before deciding +- [OAuth Approve Authorization](auth.oauth_server.approve_authorization) — the acceptance counterpart diff --git a/specs/client/authentication_integration/cross_client_token_sync.md b/specs/client/authentication_integration/cross_client_token_sync.md new file mode 100644 index 0000000..b4085d7 --- /dev/null +++ b/specs/client/authentication_integration/cross_client_token_sync.md @@ -0,0 +1,23 @@ +# Cross-Client Token Synchronization + +Keep every sub-client (database, realtime, storage, functions) authenticated with the current user's JWT automatically, without the application manually re-configuring each one after sign-in, sign-out, or token refresh. + +## Behavior + +The top-level client listens for auth state changes — sign-in, sign-out, and token refresh — and propagates the resulting JWT (or its absence, on sign-out) to every sub-client it manages. Each sub-client uses whatever token it was most recently given for its own outgoing requests; the application never calls a sub-client-specific "set token" method itself under normal use. + +The source of the JWT depends on which auth capability is active on the client: + +- Under normal Supabase Auth, the source is the SDK's own session lifecycle (sign-in/refresh/sign-out events). +- Under [Third-Party Auth](client.authentication_integration.third_party_auth), the source is the caller-supplied async token callback instead, called on the same cadence the SDK would otherwise use its own session for. + +Realtime's propagation composes with its own [Dynamic Access Token Callback](realtime.configuration.access_token_callback) mechanism — the top-level client's propagation and Realtime's per-connection refresh are two paths converging on the same "keep the socket's auth current" outcome. + +## Prerequisites + +Applies only to sub-clients constructed through the top-level client; a sub-client constructed and used standalone does not receive this propagation. + +## Related + +- [Third-Party Auth](client.authentication_integration.third_party_auth) — an alternative JWT source this propagation also serves +- [Dynamic Access Token Callback](realtime.configuration.access_token_callback) — Realtime's own token-refresh mechanism that this propagation composes with diff --git a/specs/client/authentication_integration/third_party_auth.md b/specs/client/authentication_integration/third_party_auth.md new file mode 100644 index 0000000..77cb39f --- /dev/null +++ b/specs/client/authentication_integration/third_party_auth.md @@ -0,0 +1,21 @@ +# Third-Party Auth + +Let an application supply its own external auth provider (e.g. Clerk, Auth0, a custom auth backend) instead of Supabase Auth, while still authenticating requests to the database, storage, realtime, and functions sub-clients. + +## Behavior + +At construction time, the caller provides an async callback that returns the current JWT (or `null` if unauthenticated) issued by their external provider. When this callback is supplied: + +- The SDK does not construct its own Supabase Auth client — sign-in, sign-up, session, and MFA capabilities in the `auth` area are not available on this client instance. +- Before each outgoing request, the SDK calls the callback and injects the returned JWT as the request's authorization credential, the same way it would inject a Supabase Auth session token. +- The JWT must be one Supabase's backend accepts for the project (a valid signature and claims Supabase is configured to trust) — this capability does not itself change what tokens the server accepts, only how the client sources the token it sends. + +If the callback returns `null` or rejects, the request proceeds unauthenticated (as the project's anon key) rather than failing the call outright, matching the client's behavior when no session exists under normal Supabase Auth. + +## Prerequisites + +The Supabase project must be configured to accept JWTs from the external provider (verification is a server-side project setting, not a client capability). + +## Related + +- [Cross-Client Token Synchronization](client.authentication_integration.cross_client_token_sync) — the propagation mechanism this capability relies on to reach the database/storage/realtime/functions sub-clients; when third-party auth is active, that propagation is driven by this callback instead of Supabase Auth's own session events diff --git a/specs/database/using_modifiers/relationship_embed.md b/specs/database/using_modifiers/relationship_embed.md new file mode 100644 index 0000000..8bd2703 --- /dev/null +++ b/specs/database/using_modifiers/relationship_embed.md @@ -0,0 +1,28 @@ +# Embed Related Rows + +Select columns from a related table within the same query, using PostgREST's resource embedding (spread) notation, instead of issuing a separate request per relationship. + +## Behavior + +A related table is named as a nested selection: `select("*, related_table(col1, col2)")`. The server resolves the relationship via the database's foreign keys and returns the related rows nested under the relationship name in each result row. + +- **Join direction:** by default PostgREST infers whether the embed is a to-one or to-many relationship from the foreign key direction and shapes the nested result accordingly (object vs. array). This can be made explicit with `!inner` (inner join — parent rows with no match are excluded) or `!left` (left join — parent rows are kept with a `null`/empty nested result). +- **Ambiguous relationships:** when more than one foreign key path connects two tables, the relationship must be disambiguated by naming the constraint or a hint, e.g. `related_table!fk_name(...)`. +- **Aliasing:** an embed can be renamed in the result with `alias:related_table(...)`, and the same relationship can be embedded more than once under different aliases (e.g. to apply different filters to each). +- **Nesting:** embeds can be chained arbitrarily deep — a related table's own related tables can be embedded within it, subject to the server's configured embedding depth limit. + +Filters, ordering, and column selection modifiers can be applied within a nested embed's own selection, scoped to that relationship only (they do not affect the parent selection). + +## Prerequisites + +The embedded relationship must be backed by an actual foreign key (or a PostgREST-configured view/computed relationship) that PostgREST can discover; there is no way to embed an arbitrary unrelated table. + +## Notes + +- Embedding does not change how many top-level rows are returned — it only nests additional data onto each one (except with `!inner`, which can reduce the top-level row count). +- Deeply nested or high-fanout embeds can be significantly more expensive than the equivalent client-side joins across separate queries; this capability does not impose or guarantee any particular performance characteristic. + +## Related + +- [Query Table or View](database.query.from_table) +- [Select Rows](database.query.select) diff --git a/specs/functions/invocation/streaming_response.md b/specs/functions/invocation/streaming_response.md new file mode 100644 index 0000000..ee2233b --- /dev/null +++ b/specs/functions/invocation/streaming_response.md @@ -0,0 +1,27 @@ +# Streaming Response (SSE) + +Let a caller read a function's response as a stream instead of buffering it fully before returning, when the function opts into server-sent events. + +## Behavior + +Invocation branches on the response's `Content-Type` header: + +- **`text/event-stream`:** the SDK does not buffer or parse the body. It passes through the raw response stream (e.g. the platform's native `Response`/stream type) so the caller can read server-sent events as they arrive, using whatever SSE parsing suits their use case. +- **Any other content type:** normal invocation behavior applies — the SDK buffers and decodes the body as usual (JSON, text, or binary based on content type), matching non-streaming invocations. + +This is a passthrough capability: the SDK does not parse SSE frames itself, decide when the stream ends, or retry a dropped stream. Those are the caller's responsibility once handed the raw stream. + +## Prerequisites + +The invoked function must set `Content-Type: text/event-stream` on its response for streaming behavior to trigger; there is no separate flag on the invocation call itself. + +## Notes + +- Composes with [Invocation Timeout](functions.invocation.timeout) and [Request Cancellation](functions.invocation.request_cancellation) — cancelling or timing out mid-stream should close the underlying connection the same way it would for a buffered response. +- A function that starts streaming and then errors mid-stream surfaces that as a stream-read error to the caller, not as an invocation-level error, since the SDK has already handed off the raw stream by that point. + +## Related + +- [Invoke Function](functions.invocation.invoke) — the base call this modifies +- [Invocation Timeout](functions.invocation.timeout) +- [Request Cancellation](functions.invocation.request_cancellation)