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 connect | Register with | Required? |
|---|---|---|
| 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 dropdown → NEW 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 mailGoogle People API— this is contactsGoogle 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 — 3 · Configure the OAuth consent screen
This is the screen Google shows users when they're asked to grant access. Under APIs & Services → OAuth consent screen, choose User Type External (Internal is only for Google Workspace organizations and won't let personal Gmail accounts connect), then fill in: an App name users will recognize (e.g. My freezr), your email as User support email and Developer contact. Logo, home page, privacy policy, terms and authorized domains can all be skipped — they only matter for verification.
Scopes step
This is where you tell Google what permissions your client will ever ask for. Click ADD OR REMOVE SCOPES and tick:
…/auth/gmail.readonly— reading mail…/auth/gmail.modify— marking read, labeling, moving, trashing…/auth/gmail.send— sending mail…/auth/contacts.readonly(and…/auth/contactsfor read+write)…/auth/calendar.readonly(and…/auth/calendarfor read+write)…/auth/userinfo.emailandopenid— freezr uses these to identify which account got connected
/connections/new. Leaving gmail.modify and gmail.send off the ceiling means coming back here and re-consenting later.
Test users step
This is the key step that prevents most "Access blocked" errors. Click + ADD USERS and enter your own Gmail address, plus anyone else who should be able to connect (up to 100).
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_pageURL 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.
/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.
Microsoft — 2 · Create a client secret
- Left rail of your app registration: Certificates & secrets → Client 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.
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.
/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.
| Permission | Why / what it gives you |
|---|---|
offline_access | Always needed. Issues the refresh token — without it the connection dies after ~1 hour and cannot be renewed. |
openid, email, User.Read | Always needed. Sign-in, plus reading which account connected (mailbox address and display name). |
Mail.Read | Read-only mail. |
Mail.ReadWrite | Read + mark-read, move, trash, delete, drafts. |
Mail.Send | Sending and replying. |
Calendars.Read / Calendars.ReadWrite | Calendar — read-only / create-update-delete. |
Contacts.Read / Contacts.ReadWrite | Contacts — read-only / create-update-delete. |
/connections/new. Adding the ReadWrite permissions later means re-consenting.
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 — 4 · Admin consent (work/school accounts)
Still on API permissions, you may see a banner with a button: "Grant admin consent for <Your Organization>".
- If you are a tenant admin: click it. This pre-approves the app for your whole organization and removes a consent prompt for every user. Cleanest path.
- If you are not an admin: try connecting first — many tenants let individual users consent for themselves. If the attempt fails with
AADSTS65001or "Need admin approval", your tenant has user consent disabled: send your admin the app's name and Application (client) ID and ask them to grant consent. - Personal Microsoft accounts (outlook.com etc.) have no admin and no such banner — you consent for yourself during connect. Nothing to do here.
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 sendsprompt=select_accountso 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/apps → Create New App → From 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-HOSTwith your own domain. - Click Next → Create.
- 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.
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 App → From 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.
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.
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.
| Scope | Why / what it gives you |
|---|---|
channels:read, groups:read, im:read, mpim:read | Always 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:history | Always needed. Reads the actual messages (and thread replies) in each of those four categories. |
users:read | Always needed. Turns user IDs into names — without it every message reads as "U0BRUF1A5SN" and group DMs keep their raw mpdm-… names. |
users:read.email | Recommended. Lets freezr record which account was connected, shown next to the connection name. |
chat:write | Only for Read + Write. Sending messages and thread replies, and deleting your own messages. |
channels:write, groups:write, im:write, mpim:write | Only 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.
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 URLs → Add 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.
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_urlsentry 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/resources→ Add 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.
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.
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 app | conversations.history limit |
|---|---|
| Internal app — created and installed in your own workspace, not distributed | Generous (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 it | 1 request per minute, 15 messages per call. Severe enough to make a message viewer unusable. |
| Slack Marketplace–approved app | Generous, but requires going through Slack's review process. |
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_authornot_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 needsusers: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 awww.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 index — which 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.
/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.
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 Tokens → Generate Token and Scopes →
add the
connections:writescope → Generate. Copy thexapp-…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.mpim→ Save 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.)
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→ yourslackrow → paste thexapp-…token into the App-Level Token field → save. (Token-paste-only servers that never set up OAuth: create aslackrow 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 → providerslack→ Enable.- 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 withtoken_revoked, your pasted token is stale; see the callout above. - Back on
/admin/sockets→ Start sockets. The status should show the socket connected. If Start refuses, it names exactly which of the three gates is missing.
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,
getChangesnow returns that conversation withlastActivityTsset. (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/deletedlists 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.
getChangesnow answerscomplete: 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/socketsfor 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:
| Field | Microsoft | Slack | |
|---|---|---|---|
| Type | google | microsoft | slack |
| App Name | any short label, e.g. default — only matters if you register more than one client for the same provider | ||
| Redirect URI | the exact URL you registered with the provider, character for character | ||
| Client ID (App Key) | Client ID from Google Credentials | Application (client) ID from the Entra Overview page | Client ID from Basic Information → App Credentials |
| App Secret | Client Secret from Google | the client secret Value from Certificates & secrets | Client 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/resources→ Connect 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.comand 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/resourceswith 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:
httpvshttps- Trailing slash on one side but not the other
- Port number missing or wrong
localhostvs127.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_setupdoesn'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.
How freezr uses what you set up
- Every connect flow starts at
/connections/newand ends with the provider returning the user to/public/oauth/oauth_validate_pageon 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_KEYis 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(oruse_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.