Connecting sources
A source is something tendant watches to figure out what needs doing — email, a calendar, and more over time.
How sources work
Section titled “How sources work”tendant reads from each source you connect, extracts the things that look like tasks, and keeps them in one place. You stay in control of what it can see.
Privacy
Section titled “Privacy”In keeping with the project’s principles, information only reaches an outside service when you’ve explicitly chosen to let it.
Webhooks and the Pebble voice memo
Section titled “Webhooks and the Pebble voice memo”Two sources are fed by HTTP requests you point at tendant, rather than by tendant checking a provider:
webhook-in— anything that can POST JSON. SendPOST /intake/webhook/<source id>with a body like{"idempotency_key": "evt-123", "payload": {"title": "Pay the invoice"}}(optional:disposition—forced_task,rich_eventorllm_judge;reason;raw_ref; and forrich_event,confidenceandstakes_hint).pebble-in— a Pebble/CoreApp voice memo: the device POSTs its recording to/intake/pebble/<token>, and the transcription becomes a task (or, withfilter.disposition, a candidate).
Every request must prove it knows the source’s webhook secret. Knowing the
URL is not enough, and there is no “open” mode. tendant generates a secret
(whsec_…) when you create the source; open Settings → Sources (or run the
owner-only sourceWebhookSecret(sourceId:) query) to copy it and the URL.
Then send it in any one of these ways:
| How | Header |
|---|---|
| Bearer token | Authorization: Bearer <secret> |
| Shared-secret header | X-Webhook-Secret: <secret> (for Pebble: whatever intake.pebble_secret_header names; this is the default) |
Signature (webhook-in only) | X-Signature-256: sha256=<hex HMAC-SHA256 of the raw body, keyed by the secret> — GitHub’s X-Hub-Signature-256 is accepted too |
A request without a valid secret gets 401 and nothing is stored. If the
secret leaks, rotate it (rotateSourceWebhookSecret); the old one stops
working at once. To keep a secret stable across a database reset, pin it in
tendant.toml with webhook_secret = "${env:MY_WEBHOOK_SECRET}" on the
[[sources]] entry.
For Pebble, enter the source’s id as the <token> in the device’s webhook URL
so memos go to that source exactly. With another token, tendant picks the
pebble-in source whose secret the device sent. The older instance-wide
intake.pebble_secret is still accepted alongside the per-source secrets.
Nothing is lost. tendant stores each delivery before it answers 202, so a
crash or restart right after can’t drop it; a sender that got no answer just
retries, and the retry is recognized by its idempotency_key (Pebble: the
recording) and stored once. A disabled source answers 409.
Upgrading from a release before webhook secrets: existing webhook-in and
pebble-in sources get a secret at the first start (the log names each one),
and senders that don’t send it start getting 401. Copy the secret into each
sender — or, for a Pebble already set up with intake.pebble_secret, nothing
changes.
Events relayed by homefirst.cloud (Linear, Slack, Google) don’t use these routes: the cloud checks the provider’s own signature and passes the event over the server’s authenticated connection.
Connecting to homefirst.cloud
Section titled “Connecting to homefirst.cloud”homefirst.cloud is Home First’s hosted proxy: it receives providers’ events for you (so they arrive within seconds instead of on a polling schedule) and lets you sign in to accounts with Home First’s registered apps. tendant works fully without it; every source falls back to polling.
The cloud recognizes this server, never you or your login: on first
connect tendant generates its own key pair, keeps the private half encrypted
with credentials.key, and gives the cloud only the public half. Everything
sensitive the cloud passes on — a provider’s event, a sign-in token — is
encrypted to that key before it leaves the cloud, and tendant keeps it
encrypted until the moment it’s used.
Enrolling
Section titled “Enrolling”- Set
credentials.key(TENDANT_CREDENTIALS_KEY), the cloud’s address (HOMEFIRST_CLOUD_ADDR) and this server’s id (HOMEFIRST_SERVER_ID). - Get a one-time enrollment code for the server id from whoever runs
your homefirst.cloud. An operator gets it with
pulumi stack output enrollmentCode-<server id> --show-secrets, or on the box withhomefirst-cloud enroll-code <server id>. - Either set it once as
HOMEFIRST_CLOUD_ENROLLMENT_CODE(or put it in a file and setHOMEFIRST_CLOUD_ENROLLMENT_CODE_FILE) and start tendant, or open Settings → Connected accounts and choose Enroll to paste it in — no restart needed. - Once the card shows Connected and a server key, the code is spent and tendant never needs it again: remove the environment variable. tendant never stores the code, and ignores it (logging that it can be removed) while the server is enrolled.
HOMEFIRST_CLOUD_TOKEN from earlier releases is no longer used; tendant warns
at startup while it is still set. Remove it.
The homefirst.cloud card on the Connected accounts page shows whether the server is enrolled, its key id, when the key last rotated and when it rotates next, and what to do when something’s wrong:
- Not enrolled — enter an enrollment code.
- The code isn’t valid, was already used, or has expired — ask the operator for a new code. If the server was already enrolled, a rejected code changes nothing: it keeps its key.
- homefirst.cloud no longer recognizes this server’s key (the operator removed the server, or its keys were lost) — Re-enroll with a new code from the operator. Nothing else is needed; sources keep polling meanwhile.
- Signed requests outside the cloud’s time window — this server’s clock is off. Fix it (enable NTP); tendant keeps retrying.
Key rotation
Section titled “Key rotation”tendant replaces its server key every 30 days on its own
(cloud.key_rotation_interval; 0 turns the schedule off), and you can rotate
it any time with Rotate key. A failed rotation is retried with growing
delays and shown on the card. After a rotation the old key is kept for the
cloud’s grace period (at least three days) plus one day of slack, so anything
encrypted to it still opens — including events tendant has received but not
processed yet, which keep the old key alive until they’re handled — and is
then destroyed.
Connected accounts
Section titled “Connected accounts”Some sources sign in to an account of yours: Google (Gmail, Calendar, Contacts, Drive), Slack and Linear. If this server is connected to homefirst.cloud you can connect these with a click, using Home First’s own registered apps — no developer console, no client secrets:
- Set
server.public_urlto the address you open tendant at, so the browser comes back to tendant when you’re done. - In the app, open Settings → Connected accounts and choose Connect Google, Connect Slack or Connect Linear, then pick what tendant may access. Google asks for Gmail, Calendar and Contacts by default (Drive is optional); Slack for reading, searching and sending messages as you; Linear for reading and writing issues plus live events, which needs a workspace admin.
- Approve the request in the provider’s page. The account appears in the list once the authorization completes. If it doesn’t within a few minutes (the authorization window expired, or the connection to homefirst.cloud dropped at that moment), just connect again. The same goes for “this server couldn’t open the sign-in result”: the token was encrypted to a key the server no longer holds (it was just re-enrolled) — try connecting again.
The page offers only what your homefirst.cloud is set up for: the cloud tells tendant each time it connects which providers it can sign in to, which access options each has (and the exact permissions each option grants, shown in the picker), and which sources it can push events for. A provider the cloud isn’t set up for isn’t offered; while the cloud is unreachable, nothing is.
In Settings → Sources, each source that can receive pushed events says whether it currently does (push via homefirst.cloud) or is polling, and why — for example “homefirst.cloud doesn’t offer gmail push”. If the cloud stops offering a source after it restarts, tendant keeps the source’s subscription (in case it comes back) and checks on its schedule meanwhile.
Your tokens stay at home, encrypted with credentials.key. homefirst.cloud
keeps Home First’s app secrets and never stores your tokens: when an access
token expires, tendant sends its refresh token to the cloud for that one
exchange and stores the new token. Neither side can act as you alone. If a
provider stops honoring the grant (you revoked it, or it lapsed), the account
shows Needs reconnecting — use Reconnect. Disconnect forgets the
account and its tokens here; revoke tendant’s access in the provider’s own
settings if you also want the grant gone there.
A source uses a connected account by naming it: account = "linear" (the
one Linear account), an account’s label such as you@example.com, or its id.
Several accounts of one provider
Section titled “Several accounts of one provider”You can connect as many accounts of a provider as you like — a work and a
personal Gmail, two Slack users (in the same workspace or in different ones),
two Linear workspaces. Each is its own row under Connected accounts, labelled
so you can tell them apart (the Google address; for Slack you@workspace,
e.g. brad@Acme). Reconnecting one never touches another.
Give each account its own source, naming it by its label or id
(account = "work@example.com", account = "brad@Acme"). Each source gets
its own push subscription and its own events. The provider shorthand
(account = "google", "slack", "linear") only works while exactly one
account of that provider is connected; with two it fails with an error listing
them, rather than quietly picking one. If the same item reaches two of your
accounts — a Slack message that mentions both of your Slack users, an email
sent to both mailboxes — each account’s source surfaces it once, so you get
one candidate per account (each says which inbox or Slack user it came
through). Disconnecting an account stops its sources’ push and leaves
every other account, and its sources, as they were.
Posting to Slack. The send-slack-message tool posts as a connected Slack
account — messages appear as sent by that person, and every post still goes
through your gate. Its optional account field names which one (label, id or
team:user key). With one Slack account connected it may be left out; with
several it is required, and a call without it is refused with the list of
accounts to choose from. Before the gate sees a call, tendant writes the chosen
account into it, so the approval request (and any gate script) shows exactly
which identity will post. Without a Slack account connected, the tool only logs
what it would have sent.
Linear
Section titled “Linear”tendant can watch a Linear workspace and turn what concerns you into tasks:
- An issue assigned to you becomes a task.
- A comment that mentions you becomes a candidate for you to accept or dismiss.
- A new issue in a team you watch (optional) becomes a candidate.
Add a linear source to tendant.toml that names your connected Linear
account (account = "linear"), or give it a Linear API key instead (see the
commented example in tendant.example.toml). Either way the credential is
stored encrypted. Push needs a token allowed to create webhooks: the connected
Linear account’s default scopes include it; an API key must belong to a
workspace admin.
If this server is connected to homefirst.cloud, events arrive within seconds: the cloud receives Linear’s webhook, checks its signature, and forwards it to your home over its outbound connection. Without the cloud, tendant checks Linear on the source’s schedule instead — slower, but assignments still arrive. A candidate or task carries only the issue’s identifier, title and link (plus a short snippet of a mentioning comment), never the issue’s description.
Google: Gmail, Calendar and Drive
Section titled “Google: Gmail, Calendar and Drive”With a Google account connected (see above), add a gmail, calendar or
drive source that names it — account = "you@example.com", or
account = "google" when only one Google account is connected (commented
examples are in tendant.example.toml). Drive isn’t part of the default Google
grant: tick it when you connect Google.
- Gmail — each new message matching the source’s search query becomes a task (or a candidate, if you choose). It carries the sender, subject and Gmail’s short snippet.
- Calendar — an invitation waiting for your answer becomes a task. A meeting with others that lands on your calendar, or moves, shortly before it starts (within two days by default) becomes a candidate, in case you need to prepare or rearrange. If an event tendant surfaced is cancelled, you get a candidate pointing at it. Only the title, time, organizer’s name and link are kept — never the description, location or attendees’ addresses.
- Drive — a file newly shared with you becomes a candidate (its name, type, who shared it and the link). Drive comments that mention you aren’t covered; Google emails those to you, so the Gmail source sees them.
If this server is connected to homefirst.cloud, Google tells the cloud the moment something changes, the cloud checks that the notice really came from Google and passes it on, and tendant fetches what changed straight away. The notice itself carries nothing about your mail or events — tendant reads those from Google directly, with your own token. Without the cloud (or while it’s unreachable) each source checks Google on its schedule instead; while push works it checks only occasionally, as a safety net. Each item is surfaced once, however it was noticed.
A Gmail mailbox can be watched by only one home at a time. If another tendant server already watches yours, this one keeps checking on its schedule and logs why.
With a Slack account connected (see above), add a slack source that names
it — account = "slack" when only one Slack account is connected, otherwise
its label such as account = "brad@Acme" and one source per account (a
commented example is in tendant.example.toml). tendant then sees what you can see in
Slack and surfaces what is addressed to you:
- A direct message to you from a person becomes a candidate. Many DMs are requests, many are just conversation, so tendant doesn’t decide on its own: with a triage model it judges the message, otherwise you accept or dismiss it.
- A message that mentions you (
@you, in a channel, private channel or group DM) becomes a candidate the same way — including a message someone edits to add your mention. - A reply in a thread you started becomes a low-priority candidate.
- Messages in channels you list in the source’s
channels(optional) become low-priority candidates even without a mention.
Your own messages, bots and apps, joins and leaves, @here/@channel
broadcasts, and edits or deletions of messages already seen are ignored. You
can make DMs or mentions straight tasks instead (dm_disposition,
mention_disposition). A candidate keeps only who wrote it, where (the
channel’s name, or “DM”), the first 280 characters of the message and a link
to it in Slack — never the thread, files or anyone’s email address. (Set
permalinks = "off" if you’d rather the link, which names your workspace and
channel, not be kept.)
How messages arrive. Slack sends new messages to homefirst.cloud as they are posted; the cloud checks they really came from Slack and passes them to your home over its outbound connection. Unlike Google’s notices, these carry the message text — the cloud forwards it without reading or keeping it. Each message is surfaced once, however often it is delivered.
When push isn’t available (no homefirst.cloud, or it was unreachable),
tendant catches up with Slack search on the source’s schedule: direct
messages to you and messages mentioning you since its last look (going back at
most a week, and only the newest 50 of each). While push works it still runs
every six hours (push_poll_interval) as a safety net. tendant doesn’t read
whole channel histories: Slack limits apps like Home First’s to one history
request a minute and 15 messages, so search is the only practical catch-up.
Thread replies and watched-channel messages missed while offline are not
caught up. Turn the catch-up off with backfill = "off".
If you remove Home First from Slack (revoke its access, or a workspace admin uninstalls it), Slack tells the cloud, the account shows Needs reconnecting under Connected accounts, and tendant stops listening. Reconnect Slack to pick up again.