Skip to content
Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Connectors

Show live data from your team's tools on any Space page. Declare a role in your config, call it from the page, and the account's credentials never reach the browser.

Private Beta

Connectors put live data from your team’s tools on any page of a Space, with no backend to write and no keys in the page. You declare a role in your config, bind an account to it, and the page calls /__spacefast/connectors/<role>/<tool>. Spacefast runs the tool with the account’s credentials on the server and answers the page with JSON.

Connectors are in Private Beta. A team without access gets feature_unavailable.

Declare a role

A role names one connector, the tools a page may call through it, and whose account answers. Put it under connectors in spacefast.config.ts:

import { defineConfig } from "spacefast/config";

export default defineConfig({
  connectors: {
    crm: {
      connector: "hubspot",
      tools: ["list_deals"],
      args: { list_deals: { pipeline: "sales" } },
      cache: "60s",
    },
  },
});

Or in sf.jsonc, with the same shape:

{
  "$schema": "https://spacefast.com/schemas/sf.json",
  "connectors": {
    "crm": {
      "connector": "hubspot",
      "tools": ["list_deals"],
      "args": { "list_deals": { "pipeline": "sales" } },
      "cache": "60s"
    }
  }
}

When both files exist, spacefast.config.ts wins. Publish to apply the roles: each version freezes the roles it was published with.

The tool names and arguments on this page, such as list_deals and stage, are examples. Use the names your connection lists: listConnectionTools returns each tool with its input schema.

Key Required What it does
connector yes The slug your team registered the connector under, such as hubspot. sf connectors list shows them
tools yes A list of tool names, or "*" for every tool on the connection, including tools it gains later
as no Whose account answers: team (the default), personal, or visitor. See Who reads and who writes
args no Argument values pinned per tool, applied on the server. See Pinned arguments
cache no How long viewers share one read: "30s" by default, up to a day ("1440m"). "0" turns sharing off. See Shared reads

Role names are camelCase and start with a lowercase letter, up to 64 characters. A Space declares at most 32 roles, and a role lists at most 200 tools. Tool names use letters, digits, _, ., :, and -. connect, callback, and disconnect are reserved.

Bind an account

A team or personal role needs an account bound to it. Add the connector to your team and connect an account from the team’s Connectors page, or from the CLI:

sf connectors create hubspot --slug hubspot
sf connectors connections connect hubspot

Publishing binds a role for you when exactly one eligible connection exists. The publish receipt lists the roles that still need one. Bind the rest yourself:

sf connectors bindings --space example
sf connectors bind crm cxn_abc123 --space example

In the dashboard, a team owner or admin opens the Space’s Integrations and chooses Bind account. Through the API, it’s bindSpaceConnector, and listSpaceConnectorBindings shows each role’s state.

A visitor role binds nothing. Each viewer connects their own account.

Who reads and who writes

as Whose account Who reads through it Who writes through it
team A team connection with site serving on Everyone who can open the Space A signed-in member of the team or one of the Space’s users
personal The connection of the person who bound it Everyone who can open the Space, which only works while the Space is private A signed-in member of the team or one of the Space’s users
visitor Each viewer’s own connection That viewer That viewer

The connectors route sits behind the Space’s own access check, so the data is never more public than the pages. A private Space’s data stays private. A share link or password admits anyone with the share link or the password, and they read too.

That’s why two roles have extra rules:

  • team on a Space anyone can open. When the Space is public, or shared by link or password, a team role serves only after a team owner or admin allows it: sf connectors public-serving set crm --enable --space example. Until then, calls answer connection_forbidden. The allowance covers the account bound when it was given; rebinding the role takes it away.
  • personal needs a private Space. A personal role works only while the Space is private and has no share link or password. Otherwise every call answers connection_forbidden.

Writes need a signed-in viewer. An anonymous visitor of a public Space reads through a team role and never writes through it: the call answers connector_write_sign_in_required. Send viewers to sign in with space.access.signIn(). A visitor role asks the viewer to sign in first, then to connect their account.

A team’s connector policy still applies to every call. A tool it blocks is refused, and a call it holds for approval answers approval_pending.

Call it from a page

Install the client:

npm install @spacefast/client

Then generate the typed module and call the role:

import { space } from "./spacefast.generated";

const answer = await space.connectors.crm.list_deals({ stage: "won" });
if (answer.state === "ok") render(answer.data);
else showNotice(answer.message);

Each tool knows its HTTP method. A read-only tool reads with GET, and a tool that writes sends a POST. Pass { idempotencyKey } as the last argument to make a retried write run once.

To keep a read live, watch it. A watched read refreshes on an interval and when the page regains focus, and identical reads share one request. Only reads can be watched, since every refresh would run a write again.

const deals = space.connectors.crm.list_deals.watch({ stage: "won" }, { every: "60s" });
const stop = deals.subscribe(() => render(deals.getSnapshot()));

every takes milliseconds or "30s", "5m", "1h", never under one second.

React

useConnector from @spacefast/client/react watches a read:

import { useConnector } from "@spacefast/client/react";
import { space } from "./spacefast.generated";

export function Deals() {
  const deals = useConnector(space.connectors.crm.list_deals, { stage: "won" }, { every: "60s" });
  if (deals.state === "ok") return <DealList deals={deals.data} />;
  return <p>{deals.message ?? "Loading…"}</p>;
}

It returns data, state, message, connectUrl, approvalUrl, cachedAt, refreshing, error, and refresh. data keeps the last good answer while a refresh runs or fails. Components that ask the same question share one request.

Pages without a build

A plain HTML page imports the same client from the Space itself:

<script type="module">
  import { space } from "/__spacefast/client.js";

  const { state, data, message } = await space.connectors.crm.list_deals({ stage: "won" });
  document.querySelector("#deals").textContent = state === "ok" ? JSON.stringify(data) : message;
</script>

Without generated types, every tool reads with GET. Pass { method: "POST" } to a tool that writes. The published version’s types are at /__spacefast/client.d.ts.

The route also answers plain fetch. Query parameters are string arguments (?stage=won), and __args takes a JSON object for typed values:

const args = encodeURIComponent(JSON.stringify({ stage: "won", limit: 5 }));
const response = await fetch(`/__spacefast/connectors/crm/list_deals?__args=${args}`);
const { state, data } = await response.json();

A POST sends its arguments as a JSON body of at most 64 KiB. Other sites’ pages can’t call the route with a viewer’s cookies. A browser read that says it came from elsewhere, through Sec-Fetch-Site or an Origin naming another host, answers browser_origin_not_allowed. A read that sends neither header passes that check but still goes through the Space’s access check. A write always needs an Origin naming this host or Sec-Fetch-Site: same-origin, which browsers send for you. Every answer is private, no-store.

The typed client

sf client writes spacefast.generated.ts next to your config file, typed from the connections behind your roles:

sf client

You rarely run it by hand. sf dev and sf publish regenerate the module before the build in any project that installs @spacefast/client. With roles declared, a publish whose regeneration fails stops rather than ship against stale types, and sf dev warns and keeps going. --out writes the module somewhere else.

In the generated module, pinned arguments are left out of each tool’s type. A tool name that isn’t an identifier becomes a quoted key, such as space.connectors.github["issues.list"](). A tool whose source publishes no output schema returns unknown.

What a page renders

A call answers { state, data }. When state is ok, data holds the tool’s result. Any other state carries data: null and a message you can show as is.

State What it means What the page can do
ok data is the provider’s payload. An HTTP-backed tool adds http with the upstream status and headers, and an answer served from a shared read adds cachedAt Render it
unbound No account serves this role yet Tell the viewer it isn’t set up. The Space’s owner binds an account
connect_required A visitor role needs this viewer’s account Link to connectUrl. See Visitor accounts
reconnect_required The account’s authorization expired On a visitor role, link to connectUrl. On a team or personal role, the team reconnects the account; the answer still carries a connectUrl, but it only works for visitor roles, so don’t show it
approval_pending The call is waiting for approval Say so, and link to approvalUrl when it’s there
running The call is still going Try again shortly. A watched read retries by itself

Failures a page can’t wait out come back as an RFC 9457 problem with a stable code, which the client throws as a ConnectorError (status, code, message). With useConnector, state is error and error holds it.

Code Status When
connection_forbidden 403 The team’s policy blocks the connector, a read tool, or a write on a visitor role; an approval was denied; the account doesn’t serve sites; or the Space is too open for the role
connector_role_unknown 404 The live version doesn’t declare this role
connector_tool_not_allowed 403 The role’s tools doesn’t list this tool
connector_arg_pinned 422 The call sent a different value for a pinned argument
connector_write_sign_in_required 403 A write through a team or personal role from a viewer who isn’t signed in as a member
connector_tool_blocked 403 The team’s connector policy blocks this write tool on a team or personal role
connector_write_in_read_handler 403 A GET to a tool that writes. Send a POST
browser_origin_not_allowed 403 The call came from another site, or a write without same-origin proof
connectors_unavailable 502 or 503 The provider or Spacefast couldn’t answer. Try again

Visitor accounts

A visitor role needs a connector with an OAuth sign-in method, since each viewer signs in to their own account. A connector that only takes API keys can’t serve one: connecting answers connector_oauth_required. When the connector needs an OAuth app your team hasn’t registered, it answers connector_oauth_app_required.

A visitor role’s connectUrl is /__spacefast/connectors/<role>/connect. It signs the viewer in if needed, then sends them to the provider and back. Add ?return_to=/some/path to land somewhere other than /. A POST to /__spacefast/connectors/<role>/disconnect from the Space’s own page forgets the viewer’s account.

listVisitorConnectionUsers counts which of the Space’s users connected each role, and revokeVisitorConnection disconnects one user’s account for a role.

Pinned arguments

args fixes argument values per tool on the server:

{
  "connectors": {
    "crm": {
      "connector": "hubspot",
      "tools": ["list_deals"],
      "args": { "list_deals": { "pipeline": "sales" } }
    }
  }
}

Every call to list_deals through crm runs with pipeline: "sales", whatever the page sends. A call may repeat the pinned value. A different value is refused with connector_arg_pinned, and its pointer names the argument. Pin anything a viewer shouldn’t choose, such as which pipeline, project, or account a public page shows.

Shared reads

A team or personal role shows every viewer the same account, so every viewer asks the same question. Spacefast answers them together: one upstream read serves everyone for the role’s cache window.

  • The key is the Space, role, tool, and the final arguments with pins applied. Argument order doesn’t matter.
  • One viewer fills a cold key. Others arriving meanwhile wait for that read rather than starting their own.
  • The budget is 60 upstream reads per role per rolling minute, for each Space.
  • Stale serving. When the budget is spent, the last value keeps answering for up to an hour past its window. With no value to fall back on, the call answers connectors_unavailable.
  • cachedAt appears on an ok answer served from the shared value and says when it was fetched. The read that fetches it answers without one.

Only successful reads are shared. cache: "0" turns sharing off, but each call still spends the budget. Writes, visitor roles, and tools the team’s policy holds for approval are never shared: each call runs.

Serving a team account on Spaces

A team connection reads for a Space’s viewers only once site serving is on for it. Until then, the role’s state is needs_serving_grant and calls answer connection_forbidden.

If you may turn serving on, do it from the connection in the dashboard, or with sf connectors connections update cxn_abc123 --serving. If you can’t, ask:

sf connectors serving create crm --space example

The answer is a pending request with a csr_ id, and asking again returns the same one. When you’re allowed to grant serving yourself, it turns on at once and the request comes back approved.

Team owners and admins get a notification. They decide in the dashboard, or with:

sf connectors serving list
sf connectors serving approve csr_abc123
sf connectors serving deny csr_abc123

Approving turns serving on, and the role’s state flips to ready. A team can keep serving decisions to owners and admins, and can limit which connectors may serve sites at all.

The API operations are requestSpaceConnectorServing, listConnectorServingRequests, approveConnectorServingRequest, and denyConnectorServingRequest. Public serving for a team role is setSpaceConnectorPublicServing.

Change roles from the dashboard

The Space’s Integrations page lists every role. An override lives with the Space, not in your config file, and replaces that one role:

  • Turn off removes a role your config declares. It moves to Removed roles, where Restore brings it back.
  • Use config file drops an override and returns the role to what the file says.

Saving re-applies the live version, so the change takes effect without a publish. The next publish keeps the overrides.

Through the API, overrides are config.connectors on updateSpace. An entry replaces the file’s role of the same name, and null removes it. config replaces the Space’s whole stored overlay, so read it from getSpace first and send it back with your change, along with its settingsDigest as baseSettingsDigest.

Call it from Functions

A Functions worker whose version declares roles gets env.CONNECTORS:

export async function GET(request, { env }) {
  const deals = await env.CONNECTORS.crm.list_deals({ stage: "won" });
  return Response.json(deals);
}

The role proxy returns the tool’s result, or throws a ConnectorCallError whose state.code says why, such as connect_required or forbidden. env.CONNECTORS.call("crm", "list_deals", args) returns { state, result } instead of throwing. The worker forwards the request’s signed-in viewer, so visitor roles and writes follow the same rules as a page. Calls from a worker are never shared reads.

Call it from outside the Space

The page route is for the Space’s own pages: it acts for the viewer, and other sites’ pages can’t use it. From anywhere else, such as a script, a CI job, or an agent, run the tool with your own API key through createConnectorRun:

curl -X POST https://api.spacefast.com/v1/connectors/runs \
  -H "Authorization: Bearer $SPACEFAST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":{"kind":"tool","address":"cxn_abc123/list_deals","args":{"stage":"won"}},"waitMs":10000}'

The address is <connectionId>/<tool>, straight off listConnectionTools. From the CLI, it’s sf connectors run --tool cxn_abc123/list_deals --args '{"stage":"won"}'. A run answers to your credential and the team’s policy, not to a role: no pins, no shared reads. Read the run’s status and result before treating it as done.

First

Pages in a First app call roles with @spacefast/client, like any other page. Declare the roles in spacefast.config.ts or sf.jsonc as above. First’s server code can’t call connectors yet.

Last updated on

Was this page helpful?