Adding a social provider
Contributor guide for implementing a new social integration in OpenQuok
Connect your agent today
Draft from chat, review in your calendar, and publish only what you approve.
Overview
Our social channels are provider classes registered in the backend, exposed through existing REST routes, and optionally wired in the web.
Connect is a fork of this checklist, not a separate product surface.
Pick one family:
| Family | Operator developer app? | User action | OpenQuok env keys | Reference |
|---|---|---|---|---|
| OAuth (default) | Yes | Redirect to the platform | config.integrations.* plus self-host .env.example and a docker-compose row | Threads, Instagram, Facebook Page |
| Credentials in OpenQuok | No | Paste a personal API key into Add Channel | None — do not invent empty provider env placeholders | Dev.to (devto) |
Use Facebook (facebook), Instagram (Business) (instagram-business), and Threads (threads) as OAuth references. Use Dev.to (devto) as the credentials-in-app reference.
Note
Convention reference: Contributors should follow the backend + web checklist in .cursor/rules/add-social-provider-integration.mdc alongside this guide.
OAuth:- Web calls GET /api/v1/integrations/social/:provider →
generateAuthUrl(). - User consents at the platform; browser returns to /integration/oauth/:provider.
- Web calls POST /api/v1/integrations/social-connect/:provider →
authenticate(). - If isBetweenSteps is true, response includes pages; user picks an account → POST /api/v1/integrations/provider/:id/connect →
fetchPageInformation().
- Add Channel shows a catalog-driven form from customFields() (password input + regex). Invite-link copy excludes these providers.
- Web calls GET /api/v1/integrations/social/:provider →
generateAuthUrl()seeds org/state cache. The returned url is the state string, not a platform redirect. - Web calls POST /api/v1/integrations/social-connect/:provider with that state and code as base64 JSON of the pasted key.
- authenticate() validates the key with the platform and stores it as the access token. Refresh reuses the same form — do not send
window.locationto a non-URL state.
No public OAuth URL
Session getIntegrationUrl still seeds cache for the dashboard form. GET /api/v1/public/social/:identifier returns 400 for credentials providers — connect in the dashboard with an API key. Do not add a new public connect route.
Backend checklist
1. Implement SocialProvider
Create a class under backend/integrations/providers/ implementing social.integrations.interface.ts.
Required surface:
| Member | Purpose |
|---|---|
| identifier / name | Catalog slug and display name |
| scopes | OAuth scopes (empty array for credentials providers) |
| isBetweenSteps | true when user must pick Page/account after OAuth |
| generateAuthUrl / authenticate | OAuth start + code exchange, or credentials: customFields() metadata + authenticate reading base64 JSON from code |
| post | Publish scheduled content |
| maxLength | Caption limit for API + UI |
Common optional members:
| Member | When |
|---|---|
| customFields() | Credentials-in-app connect (API key / app password). Catalog drives the Add Channel form. |
| pages() | Between-steps account list |
| fetchPageInformation() | Finalize Page/token after picker |
| refreshToken + reConnect | Long-lived token refresh (set refreshCron: true) |
| comment | Thread / follow-up replies |
| analytics / postAnalytics | Insights dashboards |
| validateCreatePost | Server-side schedule validation |
| globalPlugCatalog / internalPlugCatalog | Channel or post-compose plugs |
| settingsSchema() | Typed publish object for GET /public/integration-settings/:id |
| tools() | Allow-listed methods for POST /public/integration-trigger/:id |
2. Register the provider
Add new YourProvider() to the array in backend/integrations/integrationManager.ts.
No new API routes are required — existing integration endpoints dispatch by identifier.
3. Token refresh / between-steps storage
If OAuth returns a user token but publishing needs a Page or sub-account token, follow the Instagram (Business) / Facebook pattern in IntegrationConnectionService.saveProviderPageForOrganization:
- Store the Page token in token.
- Keep the user token in refresh_token.
- Keep the pre-picker user id in root_internal_id for reConnect during cron refresh.
Extend the preservesUserTokenForRefresh branch when adding another Meta-style provider.
4. Environment variables (OAuth only)
Skip this step when customFields() is set.
Add keys to backend/config/GlobalConfig.ts and backend/.env.development.example. If orchestrator workers need the same keys, mirror them per the orchestrator env rule.
Also add empty placeholders under Social provider apps in infra/self-host/.env.example, and document the ID/secret pair in Self-host — Docker Compose.
For credentials providers, document in that docker-compose page that the channel needs no operator app (user API key in the dashboard). Do not invent empty provider env placeholders.
5. Database
Usually no migration — integrations.provider_identifier is free text. Add migrations only for new columns, plug tables, or RLS changes.
6. Tests (when behavior is non-trivial)
Add unit tests beside the provider (OAuth or credentials connect, publish payload shaping). Extend IntegrationConnectionService.unit.test.ts when between-steps save logic differs, when customFields skips the OAuth verifier, or when a real settingsSchema() object is returned.
Web checklist
The connect catalog is backend-driven (GET /integrations). The composer is opt-in per provider.
1. Launch provider config
Add web/src/lib/ui/components/posts/providers/[id]/[id].provider.ts exporting a LaunchProviderConfig (maximumCharacters, postComment, optional checkValidity).
Register it in getLaunchProviderConfig inside providers/index.ts.
2. Preview component (recommended)
Add a provider-specific preview Svelte component and branch in ShowAllProviders.svelte.
When the provider has compose-time settings (step 6), the Post Preview column must reflect them live — not show placeholders because preview received an empty settings object.
| Layer | Action |
|---|---|
| [id]Preview.svelte | Accept a providerSettings prop; read it with read*LaunchSettings; render title, tags, cover, and other fields the Settings panel writes. |
| ShowAllProviders.svelte | Pass providerSettings on every preview branch that consumes compose settings — not only Dev.to. |
| AddEditModal.svelte | Derive effective preview settings from previewChannel.id + providerSettingsByIntegrationId and pass them to ShowAllProviders. |
| Parent modals | Do not pass previewProviderSettings from followUpTargetIntegrationId — that ID is only set for Threads/Instagram follow-up replies and leaves other providers with an empty settings object. |
2b. User docs — editor table
When composer support ships, update Writing the post (web/src/content/docs/creating-posts/writing-the-post.md) → Editor by platform.
Map SocialProvider.editor on the provider class to the Used by column:
| editor | Action |
|---|---|
| normal | Add platform to Standard row if not covered by “most social channels”; otherwise no change. |
| markdown | Append the platform name when unlocked on Markdown row (e.g. Threads when unlocked). |
| html | Append on HTML row (note if publish strips to plain text, like X). |
| none | First live Plain provider: replace “None yet” with that platform. |
3. OAuth between-steps UI
Reuse IntegrationContinue.svelte on route /integration/oauth/[provider]. When isBetweenSteps is true:
- Add a config under web/src/lib/integrations/continue-provider/ and register it in continue-provider/index.ts (title, empty-state copy, icon, and toSaveParams for saveProviderPage).
- Connect response pages is passed through ContinueIntegration.presenter.svelte.ts; the shared ContinueProviderPicker.svelte renders the list.
4. Credentials connect UI
When the catalog includes customFields:
- Show the provider in the normal Add Channel grid (AddProvider.svelte). Keep filtering these providers from invite links.
- Open a credentials dialog (password input + regex from catalog). Submit: getAuthorizeUrl then connectSocial with state = returned url and code = base64 JSON of the key.
- Reuse the same dialog from IntegrationContinue.svelte so refresh works without an OAuth redirect. Do not assign
window.location.hrefto a non-URL state string.
5. Labels and icons
Add display names to web/src/data/social-providers.ts if the slug is new. Icons may already exist for marketing placeholders.
6. Settings panel (optional)
If the provider needs compose-time options (Instagram post type, Dev.to title/tags, etc.), add Svelte settings under providers/[id]/ and wire SettingsAccordion.svelte. Emit only that provider’s bucket.
Documentation and agent resources
When shipping a user-facing provider, add:
| Artifact | Location |
|---|---|
| Setup guide | web/src/content/docs/social-integration/[id].md |
| Index LinkCard | social-integration/index.md |
| CLI examples | web/src/content/docs/cli-examples/[id].md |
| Composer editor modes | web/src/content/docs/creating-posts/writing-the-post.md — update Editor by platform Used by for the provider’s editor (normal, markdown, html, none) |
| Agent recipes | agent/skills/openquok-core/resources/[id]-examples.md |
| Identifier list | agent/skills/openquok-core/resources/patterns.md |
Follow Documentation contribution for MDX components, env badges, and redirect URI placeholders.
Reference providers
PR review prompts
Before opening a PR, confirm:
- Provider is registered in integrationManager.ts.
- OAuth: Redirect URI in the platform console matches /integration/oauth/[identifier] exactly. New operator env keys exist in infra/self-host/.env.example and the docker-compose social-apps table.
- Credentials: no new env vars; docker-compose callout that there is no operator app; dashboard Add Channel and refresh work; GET /public/social/[identifier] returns 400.
- No secrets or third-party project names in comments or docs (repo neutrality rule).
- Composer validation matches backend
validateCreatePost/ publish rules. - Post Preview: when compose settings ship, preview reads providerSettings via read*LaunchSettings; Settings changes update preview live; AddEditModal.svelte derives settings from the preview channel (not followUpTargetIntegrationId).
- Live vs Development mode called out in docs when media visibility differs.