Integrations

Flux's position is that it should not become the only place your work lives. These components join it to the systems you already use, and they are unusually blunt about which of those joins exist. Four providers are genuinely wired: Asana, Google Calendar, AssemblyAI and OpenAI. Eight more are catalogue entries with a hover state and nothing behind them. The components below draw that line where the code draws it.

What is actually connected

2 components

One panel that refuses to take your word for it. On mount it fires a real request at each provider and reports what came back, so a green tick here means a round trip succeeded a second ago, not that a setting is ticked somewhere. It is the best per-row state matrix in the app.

LiveIntegrationStatus
Four rows, four providers, four possible states each, and a detail line that changes with both. This is the panel at the top of the Integrations page, and the only place in Flux that tells you the truth about a connection without you having to try it.
src/components/integrations/live-status.tsx
checking · the state every row mounts in
Connected Services live
Real-time status probed directly against each provider's backend. Add or update your own API keys and tokens with Manage.

Asana

Checking connection…

Google Calendar

Checking connection…

AssemblyAI

Checking configuration…

OpenAI

Checking configuration…

ready · the detail line carries the count

Asana

Connected (Priya Raghavan) · 6 tasks assigned to you

Google Calendar

Connected · 14 events in the next 7 days

AssemblyAI

Transcription ready

OpenAI

AI processing ready

unconfigured · only Google Calendar offers a way out

Asana

No token configured

Google Calendar

Not connected

AssemblyAI

No API key configured

OpenAI

No API key configured

error · same glyph as unconfigured, different colour

Asana

HTTP 403

Google Calendar

Network error

AssemblyAI

Network error

OpenAI

Network error

your key · a stored credential adds a second badge

OpenAI your key

AI processing ready

Google Calendar

Connected · 1 event in the next 7 days

Google Calendar never carries this badge: it has no tokenProvider, because it is the one provider you connect by signing in rather than by pasting something. Its detail line is also the only one that singularises, so a quiet week reads "1 event" and not "1 events".

Read the row, not the panel. Every row resolves on its own, so a real screen is usually a mix and never the four tidy sets above. Two rules decide the buttons: Manage appears wherever a tokenProvider is set (Asana, AssemblyAI, OpenAI) and stays put in all four states, so you can replace a key on a row that is already green; Connect appears only where a row has an action and is not ready, which today means Google Calendar and nothing else. The error and unconfigured states share the same XCircle glyph and differ only by colour, so colour is the only cue separating "you never set this up" from "this is broken right now".

Only one row can report an HTTP failure, and the other three cannot. Asana is the sole probe with an else that reads the status back (HTTP 403 above). AssemblyAI and OpenAI branch on 501 and treat every other response as ready, so a 500 from either provider lights the row green and says "Transcription ready". Google Calendar folds every non-ok response into unconfigured, so an outage reads as "Not connected" and offers you a Connect button that will not help. In all four cases the row is honest about reaching the provider and much less honest about what it said.

A green AssemblyAI row means less than it looks like. The key is stored, the credential plumbing is real and the probe answers, but the transcription path itself is not verified end to end: no recording has been proved to travel from capture through AssemblyAI and back as a saved transcript. It is a backbone with a live socket on one end. docs/features.md marks this shipped and docs/launch-checklist.md leaves it unticked; the checklist is the one that matches the code.

Credential dialog
What Manage opens: one field for one provider's key, so you can bring your own instead of using the server's. It lives in the same file as the status panel and is driven by whichever row you clicked.
src/components/integrations/live-status.tsx
Open · no key stored yet
Manage Asana credential
Paste your Personal Access Token. It's stored securely against your account and used in place of the server default. Leave blank and save to remove a stored key.
Open · a key is already stored (the placeholder forks)
Manage OpenAI credential
Paste your API key. It's stored securely against your account and used in place of the server default. Leave blank and save to remove a stored key.
saving · both buttons disabled
Manage OpenAI credential
Paste your API key. It's stored securely against your account and used in place of the server default. Leave blank and save to remove a stored key.
saveError · the dialog stays open
Manage AssemblyAI credential
Paste your API key. It's stored securely against your account and used in place of the server default. Leave blank and save to remove a stored key.

Save failed (HTTP 500).

The placeholder is the whole disclosure. A stored key is never shown, never fetched back and never echoed: the panel only ever learns a boolean, so the field's placeholder forking from Paste your key here to •••••••• (a key is stored — enter a new one to replace) is the only way the UI admits a key exists. Closed is not a state so much as an absence: the dialog is bound to editing !== null, so nothing renders until a Manage button sets a row. On success it closes, re-reads the stored flags and re-runs every probe, so the row behind it goes green without a reload. On failure it stays open with the message under the field and both buttons live again, and the field is never cleared, so a retry does not cost you the paste.

What is not

1 component

Below the live panel sits a grid of eight providers Flux does not talk to: ClickUp, Monday.com, Notion, Microsoft Calendar, Google Tasks, Android Sleep, Gemini API and ElevenLabs. They are rendered by a card that is deliberately inert, and the code says so out loud in its own doc comment rather than leaving you to discover it by clicking.

IntegrationCard In development
A catalogue entry for a provider that is not connectable. It has a name, a glyph, a description and a hover state, and that is the entire component. Both of its states are below, side by side, because a hover state is invisible in a screenshot.
src/components/integrations/integration-card.tsx
Default
N
Notion
The all-in-one workspace for your notes, tasks, wikis, and databases.
Hover · group-hover overlay
N
Notion
The all-in-one workspace for your notes, tasks, wikis, and databases.

Exactly two states, and no third is coming without new code. The card takes three props (name, icon, description), has no click handler, no href and no connect action, and its doc comment opens with "Non-interactive". The overlay is pointer-events-none and aria-hidden, so it is decoration for sighted mouse users and does not exist for a screen reader: a keyboard or screen-reader user is never told these are unavailable, because the card offers nothing to focus in the first place. Marked In development rather than Placeholder: the card itself is finished and shipped, it is the eight providers behind it that do not exist. Both stages above are 226px wide because that is what the product gives this card: the catalog grid goes to four columns at 1280px, and CardTitle is text-2xl unmodified, so a provider name renders at 24px in a card whose description column is about 130px. The name is the largest text on the Integrations page and the sentence under it wraps two or three words at a time. That is the shipped rendering, not the stage squeezing it.

Google Calendar in detail

1 component

The deepest integration surface in the app, and a good example of the house style: four connection states, each its own card, no shared chrome between them. You never see a settings panel with an error inside it; you see an error card instead of a settings panel.

GoogleCalendarSettings
Everything you can decide about a connected Google account: which way events flow, which calendars count, what Flux is allowed to write, and what an imported event turns into. Your per-calendar choices survive a reconnect; only the calendar list itself is refetched.
src/components/settings/google-calendar-settings.tsx
loading
Google Calendar
Checking your Google Calendar connection…
not-connected
Connect Google Calendar
No Google account is connected yet. Connect one to sync your calendars with Flux.
error
Google Calendar
Something went wrong loading your calendars.

Could not reach the calendar API.

connected · ConnectedAccountCard, everything open
DA
dana.whitfield@example.com
Manage settings for this connected account.
Two-way Sync

The other two options are "Google Calendar to Flux only" and "Flux to Google Calendar only".

dana.whitfield@example.com

Choose which calendars to display and how their events are handled.

dana.whitfield@example.com

Primary
Read & Write
Block out time in Flux

Team rituals

Read-only
Import as atomic entries
connected · disabling a calendar collapses it to a row

Travel

Permissions, Import Behavior and the import button are not disabled or dimmed: the whole block is unmounted, and the card shrinks to the title row and the switch.

importing · every import button on the card goes disabled

dana.whitfield@example.com

Primary

Team rituals

Only the importing calendar swaps its icon for a spinner. The others keep the CalendarPlus glyph and go dim, because the guard is importingCalendarId !== null for all of them.

The four connection states are four separate early returns, so there is no partial render and no settings panel wrapped around a spinner. Details worth having: the account's email is not fetched from an account API, it is the summary of your primary calendar, because for Google those are the same string; the dot colours cycle a six-entry palette by list position, so an account with seven calendars gives the seventh the same blue as the first; and a calendar's default permission is derived from your real access role (owner or writer gives write, anything else read), while importBehavior defaults to block for everything. Import pulls a fixed 7 days and reports both halves of the result, including what it skipped as already linked or an all-day duplicate. What is not built is incremental sync: there is no delta or watch channel, so a change made in Google after an import does not follow it into Flux. Re-importing is the only refresh, and it is a button a human presses.