docs / mail & messaging oauth setup

Setting Up Mail & Messaging OAuth

Connecting a Gmail or Microsoft (Outlook / Microsoft 365) mailbox, or a Slack workspace, to freezr requires OAuth credentials registered with the provider. This page walks a freezr server operator through registering their own credentials with Google, Microsoft and Slack, and then wiring them into freezr. It takes about 20 minutes per provider, one time. No credit card, no paid cloud account.

Once registered, your freezr server uses the credentials transparently: users connect accounts at /connections/new, tokens are stored and refreshed server-side, and apps never see them. You can ignore the provider console afterwards unless something needs changing.

Which of these do I actually need?

You want to connectRegister withRequired?
Gmail / Google Calendar / Google Contacts Google Cloud (Google section) No. freezr ships with a default: you can authenticate through www.salmanff.com, or through any other freezr instance you choose. Registering your own is a choice for independence.
Outlook.com, Microsoft 365, Exchange Online Microsoft Entra ID (Microsoft section) Yes. There is no shared default for Microsoft — nobody hosts a public Entra app for freezr, so each server registers its own.
Slack (messaging) Slack API (Slack section) Yes. Like Microsoft, there is no shared default — each server registers its own Slack app. You can also skip OAuth entirely and paste a token, which is the quickest way to start and the only route if your server has no public HTTPS URL.
Yahoo, iCloud, Fastmail, self-hosted IMAP nothing — no OAuth involved N/A. These use an app password. Skip this whole page and use Add IMAP Mailbox on /account/resources instead. Details below.

Why register your own?

For Google it's optional — so why bother? Registering your own credentials gives you three things:

  • Decentralization. freezr's whole point is to give you control over your own data. Owning the OAuth credentials means you are the registered third party in the provider's eyes — not someone else.
  • Transparency. You can see, in Google's Cloud Console or Microsoft's Entra portal, every permission grant, every revocation, and every API call your freezr server has made on your behalf.
  • Independence. If the freezr instance you authenticate through ever changes hands, gets shut down, or loses your trust, your credentials are not entangled with it.

For Microsoft the same three benefits apply — but it isn't optional. There is no default to fall back on.

Google — 1 · Create a Google Cloud project

You'll need a Google account (typically the one whose mail you want to access) and about 20 minutes. Google Cloud's free tier covers OAuth-only usage of the Gmail / Calendar / Contacts APIs at the scale of personal mailboxes — no credit card needed.

  • Go to console.cloud.google.com and accept the terms if it's your first time.
  • Top-left, click the project dropdownNEW PROJECT.
  • Name it something memorable, e.g. freezr-mail, and click CREATE.
  • After creation, make sure the dropdown at the top shows your new project selected. Everything below assumes that.

Google — 2 · Enable the APIs

Each Google API must be explicitly enabled before freezr can talk to it. In the left rail: APIs & Services → Library, then search for and ENABLE each of:

  • Gmail API — for mail
  • Google People API — this is contacts
  • Google Calendar API

Only enable what you intend to use — if you're only doing mail, Gmail API alone is fine; the others can be added later.

Google — 4 · Create the OAuth client ID

  • APIs & Services → Credentials → CREATE CREDENTIALS → OAuth client ID.
  • Application type: Web application. (Important — Desktop / Native types won't work with freezr.)
  • Name: anything, e.g. freezr web client. Authorized JavaScript origins: leave empty.
  • Authorized redirect URIs: click + ADD URI and enter the exact callback URL of your freezr instance — see below.
  • Click CREATE, and copy the Client ID and Client Secret from the dialog. (You can re-open the client later to find them again.)
Deployed:   https://your-freezr-domain.example/public/oauth/oauth_validate_page
Local dev:  http://localhost:3000/public/oauth/oauth_validate_page

The path is always /public/oauth/oauth_validate_page. The scheme + host must match exactly (including the port for localhost). You can add several URIs — e.g. both localhost and your production URL — by clicking ADD URI again.

Google — Testing mode vs publishing

While the consent screen is in Testing mode, only the test users you listed can complete the OAuth flow. This is fine — indefinitely — for personal use, family use, or small private groups (up to 100 test users).

You only need to "publish" the app and go through Google's verification process if you want arbitrary users to be able to connect. Verification involves a privacy policy on a verified domain, a demonstration video per scope, possibly a security review for restricted scopes, and Google's review queue (usually weeks). For a single-user or family freezr it is not worth it — stay in Testing mode and add specific addresses as test users.

Google-specific problems and fixes

  • "Access blocked: app has not completed the Google verification process" — the connecting user isn't in your Test Users list. Add their address under OAuth consent screen → Test users. This applies even to the app's owner.
  • Permission errors after connecting (e.g. insufficient_scope) — either the scope isn't registered on your consent screen (add it under Scopes), or it's registered but wasn't ticked during the consent dialog — revoke at myaccount.google.com/permissions and reconnect, ticking every box Google offers.
  • "Connection has no refresh token — must reconnect" — Google issues a refresh token only on the first consent for a given user+client pair. If a stale prior grant is in the way, revoke at myaccount.google.com/permissions and connect again from scratch.

Microsoft — 1 · Register the application

freezr talks to Microsoft through the Microsoft Graph API, authenticating against the Microsoft identity platform v2.0 (Entra ID, formerly Azure AD) on the common tenant — so one registration serves both work/school accounts (Microsoft 365, Exchange Online) and personal ones (outlook.com, hotmail.com, live.com). App registration is free: a bare Entra directory (which every Microsoft 365 tenant has, and which is auto-created for personal accounts) is enough. No Azure subscription needed.

  • Go to entra.microsoft.com (or portal.azure.com → search Microsoft Entra ID). Sign in with the account you want to connect — or an admin account for your organization. Remember which account you used: the registration lives in that account's directory.
  • Left rail: Applications → App registrations+ New registration.
  • Name: something user-facing, e.g. My freezr — it appears on the Microsoft consent screen.
  • Supported account types: see the callout below — this is the single most important choice on the page.
  • Redirect URI: set the platform dropdown to Web, and enter your freezr's callback URL — the same /public/oauth/oauth_validate_page URL shown in the Google section.
  • Click Register, then copy the Application (client) ID from the Overview page. You can ignore the Directory (tenant) ID entirely — freezr uses common.
Supported account types must be multitenant + personal: choose "Accounts in any organizational directory (Any Microsoft Entra ID tenant – Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)". freezr authenticates against the /common endpoint, which only works with a multitenant registration — pick "Single tenant" and every connect attempt fails with AADSTS50194. Pick the multitenant option even if you only ever intend to connect one mailbox.
Platform matters. Web is a confidential client — it can hold a client secret, which is what freezr's server-side token exchange uses. "Single-page application" forbids secrets and would reject freezr's token request; "Public client/native" is likewise wrong.

Microsoft — 2 · Create a client secret

  • Left rail of your app registration: Certificates & secretsClient secrets tab → + New client secret.
  • Description: e.g. freezr server. Expires: pick a duration — 24 months is the maximum Microsoft allows; there is no "never expires" option.
  • Click Add.
Copy the secret Value now — it is shown exactly once. The table shows two columns, Value and Secret ID. You want Value (a long opaque string). Secret ID is a GUID and is not the secret — pasting it into freezr produces AADSTS7000215: Invalid client secret provided, the single most common setup mistake. Once you navigate away, the Value is masked forever and you have to create a new secret.
Write the expiry date down. When the secret expires, every Microsoft connection on your freezr stops refreshing its access tokens, and it surfaces to users as "your token expired, please reconnect" — which reconnecting will not fix, because the problem is server-side. Put a calendar reminder a month before expiry to rotate: create a new secret, update it at /admin/oauth_serve_setup, then delete the old one.

Microsoft — 3 · Add the Graph API permissions

Under API permissions, you'll likely see User.Read already present — keep it (freezr uses it to read which mailbox was connected). Click + Add a permission → Microsoft Graph → Delegated permissions and tick the entries below.

Delegated, not Application. Delegated permissions act as the signed-in user, which is what freezr does. Application permissions would grant access to every mailbox in the tenant, require admin consent unconditionally, and are not what freezr's flow uses.
PermissionWhy / what it gives you
offline_accessAlways needed. Issues the refresh token — without it the connection dies after ~1 hour and cannot be renewed.
openid, email, User.ReadAlways needed. Sign-in, plus reading which account connected (mailbox address and display name).
Mail.ReadRead-only mail.
Mail.ReadWriteRead + mark-read, move, trash, delete, drafts.
Mail.SendSending and replying.
Calendars.Read / Calendars.ReadWriteCalendar — read-only / create-update-delete.
Contacts.Read / Contacts.ReadWriteContacts — read-only / create-update-delete.
Tick the ReadWrite variants even if you only want to read for now. As with Google, this list is a ceiling — freezr requests a subset at connect time based on the per-service read / read+write choice on /connections/new. Adding the ReadWrite permissions later means re-consenting.
Mail only, for now. freezr's Microsoft support currently covers mail only. The calendar and contacts connectors for Microsoft aren't written yet — if you tick those services when connecting, the OAuth grant succeeds and then every calendar/contacts call fails with No calendar connector for provider: microsoft. Connect mail only until those land. (Registering the Calendars/Contacts permissions now is still harmless and saves a re-consent later.)

Microsoft — What you should NOT enable

A few Entra settings look relevant but will cause problems:

  • Authentication → Implicit grant and hybrid flows — leave both Access tokens and ID tokens unchecked. freezr uses the authorization-code flow with PKCE; implicit grant is a legacy flow and enabling it only widens your attack surface.
  • Authentication → Allow public client flows — leave set to No. freezr is a confidential client with a secret.
  • Application permissions (as opposed to Delegated) — don't add any.
  • Expose an API / App roles / Token configuration — not used by freezr; skip entirely.

Microsoft-specific problems and fixes

  • AADSTS50194 — not configured as a multi-tenant application. Your registration is single-tenant but freezr authenticates against /common. Fix: app registration → Authentication → Supported account types → switch to "Accounts in any organizational directory and personal Microsoft accounts" → Save.
  • AADSTS7000215 — invalid client secret. Almost always: (1) you pasted the Secret ID instead of the secret Value — create a new secret and copy the Value column; (2) the secret expired — check Certificates & secrets, create a new one and update /admin/oauth_serve_setup; or (3) a stray space or newline got copied along.
  • AADSTS50011 — redirect URI does not match. The registered URI doesn't byte-for-byte match what freezr sent. Microsoft prints the URI it received — see redirect URI mismatch.
  • AADSTS65001 — user or admin has not consented. Your tenant requires admin consent for this app — see admin consent.
  • AADSTS90002 / AADSTS900023 — tenant not found. Usually the signing-in account doesn't live in the directory you think it does. Sign in with the exact mailbox address you want to connect — freezr sends prompt=select_account so you can pick.
  • "I registered the app but can't find it in App registrations." You're probably looking at the wrong directory or signed in with a different account than the one that registered it. Enterprise applications entries are not the registration — they're per-tenant instances created on first consent, and have no redirect-URI settings. Copy the Application ID from the Enterprise applications overview, then check App registrations → All applications in each of your directories (profile picture → Switch directory), searching by that ID — or sign in with the account that created it.
  • "Disconnect" in freezr didn't revoke access at Microsoft. Correct and expected — the Microsoft identity platform v2.0 has no public token-revocation endpoint, so freezr's disconnect deletes the local connection record and its tokens (which is sufficient for freezr to lose access) but the grant survives on Microsoft's side. To fully withdraw it: personal accounts at account.live.com/consent/Manage; work/school accounts at myapplications.microsoft.com or via your admin. (Google's disconnect revokes at both ends.)
  • Sending a large attachment fails. freezr's Graph connector attaches files in the same request as the draft, and Microsoft caps that at roughly 3 MB per attachment. Larger files need Graph's chunked upload sessions, which aren't implemented yet. Gmail and IMAP accounts don't have this limit.

Slack — 1 · Create the app

freezr talks to Slack through the Slack Web API using a user token — a token that sees exactly what the person who authorized it sees: every channel they're a member of, their DMs and group DMs. Creating a Slack app is free and takes a couple of minutes.

Slack lets you create an app from a manifest — a single block of JSON describing the redirect URL and every scope at once. That is by far the quickest route and it removes the two most common setup mistakes (ticking Bot scopes instead of User scopes, and forgetting to save the redirect URL), so it's the recommended path.

Option A — from a manifest (recommended)

  • Go to api.slack.com/appsCreate New AppFrom a manifest.
  • Pick the workspace the app will belong to. This choice matters more than it looks — see distribution & rate limits.
  • Choose the JSON tab, paste the manifest below, and replace YOUR-FREEZR-HOST with your own domain.
  • Click NextCreate.
  • From Basic Information → App Credentials, copy the Client ID and Client Secret — these are what you'll register on freezr.
{
  "display_information": {
    "name": "freezr connector"
  },
  "oauth_config": {
    "redirect_urls": [
      "https://YOUR-FREEZR-HOST/public/oauth/oauth_validate_page"
    ],
    "scopes": {
      "user": [
        "channels:read",
        "groups:read",
        "im:read",
        "mpim:read",
        "channels:history",
        "groups:history",
        "im:history",
        "mpim:history",
        "users:read",
        "users:read.email",
        "chat:write",
        "channels:write",
        "groups:write",
        "im:write",
        "mpim:write"
      ]
    }
  },
  "settings": {
    "org_deploy_enabled": false,
    "socket_mode_enabled": false,
    "token_rotation_enabled": false
  }
}

That manifest grants read + write. For a read-only connection, drop the last five scopes (chat:write and the four *:write entries) — everything above them is read-only. What each scope buys you is spelled out in the next section.

Why these settings matter. token_rotation_enabled: false keeps Slack issuing a non-expiring user token, which is what lets freezr store it without a refresh token (and what makes the paste-a-token shortcut possible). org_deploy_enabled: false keeps the app a plain internal app — the tier with usable rate limits. socket_mode_enabled: false is right for reading and sending; flip it on later only if you set up live updates, which has its own manifest addition. The scopes go under "user", never "bot".

Option B — from scratch (manual)

  • At api.slack.com/apps click Create New AppFrom scratch.
  • App Name: something user-facing, e.g. My freezr — it appears on Slack's authorization screen. Pick the workspace.
  • Click Create App, then add the user token scopes and the redirect URL by hand — the two sections that follow.
  • From Basic Information → App Credentials, copy the Client ID and Client Secret.

You can also paste the manifest into an existing app: App Manifest in the left rail, edit, and save. Slack will prompt you to reinstall, which issues a new token.

You need permission to install apps. On many company workspaces, installing an app requires workspace-admin approval. If you don't own the workspace, expect an approval request rather than an instant install — and on a locked-down workspace it may be declined outright. Creating the app is always allowed; installing it is what's gated.

Slack — 2 · The user token scopes

If you created the app from the manifest, these are already set — read on only to understand what you granted, or to trim the list. To set them by hand: OAuth & Permissions → Scopes. There are two boxes on that page, and picking the wrong one is the single most common mistake — everything below goes in User Token Scopes.

User Token Scopes, not Bot Token Scopes. A bot token (xoxb-…) only sees channels the bot has been explicitly invited to, cannot read your DMs, and cannot mark messages read at all. freezr's Slack connector is built entirely around the user token (xoxp-…). If you fill in the Bot box instead, the connection will authorize successfully and then show almost nothing.
ScopeWhy / what it gives you
channels:read, groups:read, im:read, mpim:readAlways needed. Lists the conversations you belong to — public channels, private channels, DMs and group DMs respectively. Drop one and that whole category disappears from the sidebar.
channels:history, groups:history, im:history, mpim:historyAlways needed. Reads the actual messages (and thread replies) in each of those four categories.
users:readAlways needed. Turns user IDs into names — without it every message reads as "U0BRUF1A5SN" and group DMs keep their raw mpdm-… names.
users:read.emailRecommended. Lets freezr record which account was connected, shown next to the connection name.
chat:writeOnly for Read + Write. Sending messages and thread replies, and deleting your own messages.
channels:write, groups:write, im:write, mpim:writeOnly for Read + Write. Needed to move the read cursor — i.e. the "mark unread" action.

If you only ever want to read, the chat:write and *:write rows can be left out — pick Read only when you create the connection and freezr won't request them.

Adding scopes later means reinstalling. Scopes are baked into the token at install time. If you add one afterwards, Slack shows a "reinstall your app" banner — and reinstalling issues a new token, so a connection created from a pasted token must be disconnected and re-added with the new one. Getting the list right the first time saves that round trip.

Slack — 3 · The redirect URL

The manifest sets this too — check it picked up your host correctly and skip ahead. To add it by hand: under OAuth & Permissions, find Redirect URLsAdd New Redirect URL and enter your freezr's callback:

  • https://your-freezr-domain/public/oauth/oauth_validate_page

Click Add, then Save URLs — it is easy to add the URL and navigate away without saving, which produces a redirect mismatch later. The URL must match what freezr sends character for character, including www. or its absence; see redirect URI mismatch.

Slack requires HTTPS — there is no localhost exemption. Google and Microsoft both allow http://localhost redirect URLs for development; Slack does not accept http:// at all. If you're running freezr locally you cannot complete the OAuth flow without putting a public HTTPS tunnel in front of it. This is exactly what the token shortcut below is for.

Slack — Shortcut: connect by pasting a token

Slack is the one provider where you can skip the OAuth dance entirely. Because a Slack user token doesn't expire, freezr can store one you paste in directly — no redirect URL, no client secret, no HTTPS requirement. This is the fastest way to get going, and the only way if your freezr has no public HTTPS address.

  • Create the app with the manifest (or add the scopes by hand). The redirect_urls entry is unused on this route, so any placeholder host will do.
  • On OAuth & Permissions, click Install to Workspace (top of the page) and approve the authorization screen.
  • Copy the User OAuth Token — it starts with xoxp-. It appears on that same page after installing.
  • On your freezr, go to /account/resourcesAdd Slack (paste token).
  • Give it a connection name (letters, digits, underscore, dash — e.g. slackWork), paste the token, and choose Read & write or Read only.
  • Click Save, then open it at /connections/messaging.
Copy the User OAuth Token, not the Bot User OAuth Token. The page shows both when the app has bot scopes. freezr rejects anything that doesn't start with xoxp- and will tell you if you paste a xoxb- bot token by mistake. Also ignore the Generate Token button on Slack's app-configuration tooling page — those are app-configuration tokens (xoxe-) for managing app manifests and cannot read messages at all.
The token is stored encrypted, in the same per-user resources store as IMAP passwords and OAuth refresh tokens, and is never exposed to apps. Apps ask for use_messaging permission on a named connection and freezr makes the calls on their behalf. Treat the token like a password while it's on your clipboard — it grants everything your scopes list allows.

Tokens created this way work identically to OAuth-created ones everywhere else in freezr. The trade-off is manual lifecycle: if you revoke or reinstall the app the token stops working, and you disconnect and paste the new one rather than clicking reconnect.

Slack — Distribution and the rate-limit trap

This is the most consequential thing to understand about Slack, and it has no equivalent on Google or Microsoft. In May 2025 Slack sharply reduced API rate limits for apps that are distributed but not approved for the Slack Marketplace:

Kind of appconversations.history limit
Internal app — created and installed in your own workspace, not distributedGenerous (roughly 50+ requests/minute, up to 200 messages per call). This is what freezr assumes.
Distributed, non-Marketplace app — public distribution enabled so other workspaces can install it1 request per minute, 15 messages per call. Severe enough to make a message viewer unusable.
Slack Marketplace–approved appGenerous, but requires going through Slack's review process.
Don't enable public distribution just to let other people connect. It is the natural-looking move — "Manage Distribution → Activate Public Distribution" so users in other workspaces can install — and it silently drops your app into the 1-request-per-minute tier. Everything will still authorize; reading a channel will simply crawl.

The model that works, and that happens to match freezr's whole premise, is one Slack app per workspace, owned by that workspace. If you run a freezr for several people in the same workspace, one internal app serves all of them — each user authorizes it separately and gets their own token scoped to what they personally can see. If you want people from other workspaces to connect, have each of them create their own Slack app in their own workspace and register it on their own freezr, rather than distributing yours.

Slack's own write-ups: the original changelog and a follow-up clarifying that internal apps are unaffected.

Slack-specific problems and fixes

  • invalid_auth or not_authed. The token is wrong, revoked, or the app was reinstalled (which issues a new token and invalidates the old one). For a pasted-token connection: disconnect it and add the new token. freezr surfaces this as the usual "needs reconnect" banner.
  • missing_scope. The message names the scope Slack wanted. Add it under User Token Scopes, reinstall the app, and — for pasted tokens — replace the token, since reinstalling issues a new one.
  • Channels are missing from the sidebar. Two expected causes. Slack only returns conversations you are a member of, so public channels you've never joined won't appear no matter what scopes you grant — join them in Slack first. And archived channels are excluded by default. If a channel you are in is missing, use the filter box to check before assuming it's a bug: large workspaces load thousands of conversations, sorted alphabetically within Channels and Direct messages.
  • Group DMs show as mpdm-alice--bob--carol-1. That is Slack's internal name for a group DM. freezr resolves it to real names via a members lookup, which needs users:read — without that scope the raw name is the best it can do.
  • cant_delete_message. Expected. Slack only lets a user token delete that user's own messages. Bulk delete reports these as "skipped" rather than failing.
  • "Mark unread" says it needs an older message. Slack has no per-message unread flag — read state is a single per-conversation cursor. To mark a message unread, freezr moves the cursor to the message before it, so it needs that message loaded. Scroll up (or press Get older) and try again.
  • No refresh token — is that a problem? No. Unless you deliberately enable token rotation on the Slack app, Slack issues a user token that simply doesn't expire, and freezr stores it accordingly. Google and Microsoft both require refresh tokens; Slack doesn't.
  • The OAuth flow fails but pasting a token works. Almost always the redirect URL: not saved, missing https://, or a www. mismatch between what you registered and the address you're actually browsing. See redirect URI mismatch.

Slack — Live updates (sockets): what and why

Everything above lets apps read Slack — but Slack's API has no way to ask "what changed anywhere since yesterday?", so an app syncing in the background would have to poll every channel one by one. Live updates solve this: your freezr server holds a persistent connection (a WebSocket, via Slack's Socket Mode) and Slack pushes each message event as it happens. freezr records only an activity indexwhich conversation changed, when, and which message ids were edited or deleted. No message content is ever stored on the server. Apps ask "what changed since X?" and then fetch the actual messages with the user's own token, so Slack itself remains the enforcer of who can read what.

Because the connection is outbound, this works anywhere — including localhost, with no HTTPS requirement (unlike the OAuth redirect). It also works fine with token-pasted connections.

Three separate switches, deliberately. Holding sockets is a server capability, so it is admin-gated twice and user-gated once: (1) the admin master switch on /admin/prefs; (2) an explicit admission per provider on /admin/sockets — registering the credential does not enable the socket, the admission does; (3) each user opts their own connection in ("Live updates" on /account/resources or /connections/edit). Apps get no socket rights at all — they only read the index through their normal use_messaging permission.
Gaps are tracked honestly. Slack does not replay events missed while the socket was down (though brief outages often self-heal — Slack retries undelivered events for a short while on reconnect). freezr records every downtime window, and the getChanges API answers complete: false whenever a gap overlaps the window you asked about — the app then does a normal wider fetch. The index is an optimization, never the source of truth.

Slack — Live updates: setup

On the Slack app (api.slack.com/apps → your app):

  • Settings → Socket Mode → enable.
  • Basic Information → App-Level TokensGenerate Token and Scopes → add the connections:write scope → Generate. Copy the xapp-… token. This is a third kind of token — not the user token (xoxp) and not a bot token: it only opens the socket.
  • Event Subscriptions → enable → expand "Subscribe to events on behalf of users" (NOT the bot events box above it) → add message.channels, message.groups, message.im, message.mpimSave Changes.
  • Reinstall the app when Slack's banner asks.

Or do steps 1 and 3 in one paste — App Manifest in the left rail, replace the settings block with:

"settings": {
  "event_subscriptions": {
    "user_events": [
      "message.channels",
      "message.groups",
      "message.im",
      "message.mpim"
    ]
  },
  "org_deploy_enabled": false,
  "socket_mode_enabled": true,
  "token_rotation_enabled": false
}

(The app-level token can't go in the manifest — generate it by hand, step 2.)

Reinstalling re-issues the User OAuth Token. Any freezr connection created by pasting the old xoxp-… stops working the moment you reinstall (token_revoked). Refresh the OAuth & Permissions page (it caches the old token on screen!), copy the new token, disconnect the old freezr connection and re-add it — then re-enable its Live updates toggle. Do the Slack-side setup FIRST, and connect freezr after, to avoid doing this twice.

On freezr (admin steps, then one user step):

  • /admin/prefs → Background Jobs → tick "Enable messaging sockets" → save.
  • /admin/oauth_serve_setup → your slack row → paste the xapp-… token into the App-Level Token field → save. (Token-paste-only servers that never set up OAuth: create a slack row now — Client ID / Redirect URI can be placeholders; only the App-Level Token matters for sockets, but the row must be enabled.)
  • /admin/sockets → Admissions → provider slackEnable.
  • As the user: /account/resources → your Slack connection → "Live updates: off" → click to turn ON. (Also available on /connections/edit.) This resolves your workspace identity with the connection's own token — if it errors with token_revoked, your pasted token is stale; see the callout above.
  • Back on /admin/socketsStart sockets. The status should show the socket connected. If Start refuses, it names exactly which of the three gates is missing.
Starting is manual (for now). The socket manager does not yet auto-start at boot: after every server restart or redeploy, press Start sockets again. Stopping the server or the manager just creates a recorded gap — apps are told, nothing is silently wrong.

Slack — Live updates: verify it works

Ten minutes, two browser tabs. Anyone deploying this should run the sequence once — it exercises every part of the pipe, including the failure honesty.

Tab 1: /admin/sockets with auto-refresh ticked. Tab 2: /connections/messaging with DevTools open — the index is queried from its console with:

await freezr.connections.messaging.getChanges({ connectionName: 'YOUR-CONNECTION-NAME' })
  • Delivery. Post a message in any Slack channel or DM you're in. On tab 1 the socket row's events count ticks and the routed events line increases. In tab 2, getChanges now returns that conversation with lastActivityTs set. (Events counting but landing in unrouted instead = the Live-updates toggle registered a different identity than the app is delivering for — usually a stale connection.)
  • Edits and deletions. Edit an old Slack message, delete another. The conversation's edited / deleted lists now carry those message ids with the time the change happened — this is what lets an app that stored the old version update or tombstone it.
  • The gap contract. Press Stop sockets, post a message in Slack, press Start sockets. getChanges now answers complete: false — "my index can't be trusted for this window" — and the app's remedy is a normal fetch. (Don't be surprised if the missed message then arrives anyway a minute later: Slack retries briefly-undelivered events on reconnect. Treat that as luck, not a guarantee.)
  • The opt-out. Turn the connection's Live updates OFF, post a message: it shows up as unrouted on tab 1 and never enters your index. Routing is fail-closed — events that match no opted-in user are counted and dropped, never guessed at.

Production / hosted servers (e.g. Azure App Service):

  • Always On must be enabled (Basic tier or above). Without it the host unloads the app after ~20 idle minutes, killing the socket until the next visit.
  • Run one instance. Two instances would each open the socket and write duplicate index entries.
  • Watch reconnects/hour on /admin/sockets for a day or two after deploying. A few reconnects per day is normal (Slack routinely cycles sockets, with warning, and the manager reconnects); a reconnect every few minutes means the host is killing idle outbound connections and this feature will miss events on that hosting.
  • Press Start after every deploy or restart — see the note above.
  • The routed / unrouted / write errors line is the ongoing health check: routed should grow with real traffic, unrouted should stay near zero once every active user has opted in, and write errors should be zero.

Back on freezr — Register the credentials

Identical for all three providers. Log into your freezr as an admin user, visit /admin/oauth_serve_setup, and under Create New OAuth Configuration fill in:

FieldGoogleMicrosoftSlack
Typegooglemicrosoftslack
App Nameany short label, e.g. default — only matters if you register more than one client for the same provider
Redirect URIthe exact URL you registered with the provider, character for character
Client ID (App Key)Client ID from Google CredentialsApplication (client) ID from the Entra Overview pageClient ID from Basic Information → App Credentials
App SecretClient Secret from Googlethe client secret Value from Certificates & secretsClient Secret from Basic Information → App Credentials

Slack connections added with the token shortcut need none of this — there's no client ID or secret involved, so you can skip this step entirely for those.

Click Create OAuth Permission — the new entry should appear in the list above the form, marked enabled: true. Leave the Federation section alone unless you specifically want other freezr instances' users to authenticate through your credentials; by default your credentials serve only your own freezr's users, which is what almost everyone wants.

Back on freezr — Connect an account

  • Go to /account/resourcesConnect Account, or straight to /connections/new.
  • Provider: Google, Microsoft or Slack.
  • Connection name: a short handle you'll see everywhere afterwards — gmailWork, msWork, slackWork. Letters, digits, underscore and dash only; unique per user. Apps request access by this name, so pick something you'll recognize in a permission dialog a year from now.
  • Services: tick the services you want and pick Read only or Read + Write for each — this determines the subset of scopes freezr actually asks the provider for. The list adapts to the provider: mail / calendar / contacts for Google and Microsoft, messaging for Slack. (For Microsoft today: tick mail only — see the note above.)
  • Authenticator: pick "This freezr's own credentials" — that's the row you just created. "Default" delegates to www.salmanff.com and works for Google only — Microsoft and Slack have no default provider, so freezr greys that option out for them. "Custom (another freezr)" authenticates through another freezr instance you trust — the decentralized option.
  • Complete the provider's consent screen, and you land back on /account/resources with the new connection listed.

Open a mail connection at /connections/mail and a Slack one at /connections/messaging — or use the Open Mail / Open Messaging links beside each entry in Connected Accounts.

Redirect URI mismatch (both providers)

The URL registered with the provider doesn't exactly match what freezr sent. Common gotchas, in rough order of frequency:

  • http vs https
  • Trailing slash on one side but not the other
  • Port number missing or wrong
  • localhost vs 127.0.0.1 — these are different to both providers
  • Wrong path — it is always /public/oauth/oauth_validate_page
  • You registered the URI but forgot to click Save in the provider's console
  • The Redirect URI field at /admin/oauth_serve_setup doesn't match the provider console — freezr sends what's in its field, so both must agree

Fix: read the exact URI out of the provider's error message and add it, verbatim, to the provider console. Both Google and Microsoft allow several redirect URIs on one client, so you can keep a localhost entry and a production entry side by side.

Other shared problems

  • No enabled OAuth configuration found for type: microsoft (or google) — there's no row for that provider at /admin/oauth_serve_setup, or the row is disabled. For Google you can dodge this by choosing the "Default" authenticator instead; for Microsoft there's no default — create the row.
  • You changed your freezr's domain or port — two places to update, both required: the provider console (add the new redirect URI alongside the old one) and the row's Redirect URI at /admin/oauth_serve_setup. Existing connections keep working — only new connects and re-auths go through the redirect URI.
  • "Token expired, please reconnect" that reconnecting doesn't fix — if it affects one connection, it's a genuinely revoked or lapsed grant: reconnect. If it affects every connection for a provider at once, the problem is server-side: the client secret expired (very likely on Microsoft) or the /admin/oauth_serve_setup row was deleted or disabled. Check those before debugging anything user-facing.

How freezr uses what you set up

  • Every connect flow starts at /connections/new and ends with the provider returning the user to /public/oauth/oauth_validate_page on your freezr.
  • The flow is authorization code + PKCE — freezr generates a code verifier per attempt, so the authorization code is useless without it even if intercepted.
  • Tokens are stored on your freezr server, encrypted at rest if FREEZR_ENV_KEY is set in the server's environment (strongly recommended for production).
  • The tokens never leave your server. Apps request mail / calendar / contacts data through server-side APIs that mediate access and enforce per-app permissions — an app never sees an access token. Access tokens refresh automatically and silently.
  • Each app additionally needs a use_mail (or use_contacts / use_calendar) permission grant, where the user chooses which connections it may see and whether it gets read or write. Connecting an account does not by itself expose it to any app.
  • Withdraw access any time: Disconnect in freezr at /account/resources (always removes freezr's copy of the tokens; also revokes provider-side for Google — not for Microsoft, see above), or revoke from the provider's side directly.

Yahoo, iCloud, Fastmail & self-hosted IMAP

These don't need any of this. freezr's IMAP connector authenticates with an app password — a provider-generated password scoped to one application. Use Add IMAP Mailbox on /account/resources and enter the IMAP and SMTP host/port plus that app password; freezr has presets for the common providers. (Yahoo's OAuth path requires a signed Commercial Access Agreement, which is why the app-password route exists.)

For anything else with an OAuth server, the general idea on this page carries over — register your own client, register the redirect URI /public/oauth/oauth_validate_page, paste the credentials into /admin/oauth_serve_setup — but freezr also needs a provider module and a data connector per service before a new provider type works. Today the registered connection providers are google and microsoft.

Last updated: August 2026
Both provider consoles get periodically reorganized — treat menu paths as approximate and the field names as reliable.