Overview
OAuth connectors allow Base44 apps to integrate with external services (Google Calendar, Slack, LinkedIn, etc.) via OAuth. This document describes the proposed CLI resource implementation.
Implementation Tasks
Backend (apper)
CLI
Task Details
| Task |
Description |
Blocked By |
| Task 0 |
Backend /sync endpoint with exact scope matching |
— |
| Task 1 |
Zod schemas for connector resources, JSONC file reader |
— |
| Task 2 |
API client: syncConnector(), listConnectors(), getOAuthStatus(), removeConnector() |
Task 0 (for sync) |
| Task 3 |
Compare local vs upstream, determine actions (sync/delete) |
Task 0 |
| Task 4 |
Display auth URL, poll for completion, handle edge cases |
Task 2 |
| Task 5 |
Wire into push command, colored summary output |
Tasks 3, 4 |
Key Concepts
| Term |
Description |
| AppIntegration |
MongoDB model storing connector state per app |
| Composio |
Third-party service handling OAuth flows, token storage, and refresh |
| Connected Account |
A user's authorized connection to an external service |
Supported Integration Types
googlecalendar, googledrive, gmail, googlesheets, googledocs, googleslides,
slack, notion, salesforce, hubspot, linkedin, tiktok
Connector States
| Status |
Description |
ACTIVE |
Connected and functional |
DISCONNECTED |
Soft-deleted, preserves scopes for reconnection |
EXPIRED |
Token expired, needs re-auth |
CLI Resource File Format
File Structure
connectors/
├── googlecalendar.jsonc
├── slack.jsonc
└── notion.jsonc
Resource Schema
- One file per connector, named by integration type
- Stateless - contains only type and scopes, no status field
- Uses JSONC (JSON with comments) for consistency with other CLI resources
- No pull support - connectors are push-only
Push Flow Logic
The /sync Endpoint
The CLI uses a dedicated /sync endpoint designed for declarative state management. Unlike the /initiate endpoint (used by Apper's AI for incremental scope expansion), /sync provides:
- Exact scope matching - returns
already_authorized=true only when scopes match exactly
- Scope replacement - never merges scopes, always replaces with requested scopes
- Built-in scope enhancement - backend handles auto-added scopes internally
Comparison Matrix
| Local File |
Upstream State |
Scopes Match? |
Action |
| Exists |
Not exists |
N/A |
Call /sync → auth URL + poll |
| Exists |
DISCONNECTED |
N/A |
Call /sync → auth URL + poll |
| Exists |
EXPIRED |
N/A |
Call /sync → auth URL + poll |
| Exists |
ACTIVE |
Yes |
Call /sync → already_authorized=true (no-op) |
| Exists |
ACTIVE |
No |
Call /sync → auth URL + poll (scopes replaced) |
| Not exists |
Exists (any) |
N/A |
Hard delete upstream |
How /sync Works
- CLI sends
{ integration_type, scopes } to /sync
- Backend enhances scopes internally (adds identity scopes like
email)
- Backend compares
enhanced(requested) with existing approved scopes (exact equality)
- If exact match → returns
{ already_authorized: true }
- If different → replaces auth config scopes, creates new connection, returns
{ redirect_url, connection_id }
This is simpler than the AI flow because:
- No client-side scope enhancement needed
- No delete-then-create dance
- Single atomic operation
Post-Auth Verification
After OAuth completes, verify the approved scopes match what was expected:
- Fetch connector from
/list endpoint
- Compare
approved_scopes with requested_scopes from JSONC
- If not equal → show
SCOPE_MISMATCH (user gave partial consent)
Note: The /list endpoint returns the enhanced scopes (including auto-added). The CLI should compare against the full approved set, not just the user-specified scopes.
Possible outcomes:
| Approved vs Expected |
Result |
| Scopes match |
✓ ACTIVE |
| User deselected scopes during consent |
⚠ SCOPE_MISMATCH |
Auth Flow Behavior
- Sequential auth: If multiple connectors need auth, prompt one at a time (interactive state machine)
- Polling timeout: Same as device login auth timeout
Push Summary Output
Uses @clack/prompts logging with colors (consistent with existing CLI patterns):
Connectors push summary:
- googlecalendar: active (3 scopes)
- slack: active (4 scopes, re-authed)
- linkedin: scope mismatch (requested 5, approved 3)
- notion: auth not completed
- hubspot: deleted (no local definition)
Some connectors need attention:
- linkedin: Approved scopes differ from requested. Update connectors/linkedin.jsonc or run push again.
- notion: Authentication not completed. Run push to retry.
Status text will be colored via chalk:
- active → green
- scope mismatch → yellow
- auth not completed / failed → red
- deleted → dim/gray
Summary Status States
| Status |
Color |
Description |
ACTIVE |
green |
Connector active, scopes match |
SCOPE_MISMATCH |
yellow |
Active but approved scopes ≠ requested (partial consent) |
PENDING_AUTH |
red |
Auth URL shown but user didn't complete |
AUTH_FAILED |
red |
OAuth flow failed |
DELETED |
dim |
Removed from upstream (no local file) |
DIFFERENT_USER |
red |
Another user already authorized this connector |
API Endpoints
| Endpoint |
Method |
Description |
/api/apps/{app_id}/external-auth/sync |
POST |
CLI endpoint - sync connector with exact scopes |
/api/apps/{app_id}/external-auth/list |
GET |
List all connectors |
/api/apps/{app_id}/external-auth/status |
GET |
Poll for OAuth completion |
/api/apps/{app_id}/external-auth/integrations/{type}/remove |
DELETE |
Hard delete |
Sync Endpoint (CLI-specific)
POST /api/apps/{app_id}/external-auth/sync
{
"integration_type": "googlecalendar",
"scopes": ["https://www.googleapis.com/auth/calendar.readonly"]
}
// Response - needs auth
{
"redirect_url": "https://accounts.google.com/o/oauth2/...",
"connection_id": "abc123",
"already_authorized": false
}
// Response - already authorized with exact scopes
{
"redirect_url": null,
"connection_id": null,
"already_authorized": true
}
// Response - different user error
{
"redirect_url": null,
"connection_id": null,
"already_authorized": false,
"error": "different_user",
"error_message": "Integration already authorized by another user",
"other_user_email": "other@example.com"
}
Key differences from /initiate:
- Exact scope comparison (not subset)
- Replaces scopes (not merges)
- Handles scope enhancement server-side
Polling
GET /status?integration_type=googlecalendar&connection_id=abc123
→ {"status": "ACTIVE" | "FAILED" | "PENDING"}
Poll until ACTIVE or FAILED. Suggested interval: 2 seconds.
Edge Cases
-
Partial Consent: OAuth provider may approve fewer scopes than requested → show SCOPE_MISMATCH
-
Different User: Only one user can auth per connector per app. If another user already authorized, show their email and suggest they disconnect first.
-
Scope Validation: Scopes are NOT validated by apper - the OAuth provider validates during consent. Invalid scopes cause errors on the provider's consent screen.
-
Provider doesn't return scopes: Some OAuth providers (e.g., Facebook) don't return approved scopes in token response. Apper falls back to requested (enhanced) scopes.
Auto-Added Scopes (Reference)
These scopes are added automatically by the backend for identity extraction. Users don't need to include them in JSONC files - the /sync endpoint handles enhancement internally.
| Integration |
Auto-added Scopes |
| All Google (Calendar, Drive, Gmail, Sheets, Docs, Slides) |
email |
| Slack |
users:read, users:read.email |
| Salesforce |
openid, profile, email |
| HubSpot |
oauth |
| LinkedIn |
openid, profile, email |
| TikTok |
user.info.basic |
| Notion |
(none) |
Known Scopes Reference
Google Calendar
| Scope |
Description |
https://www.googleapis.com/auth/calendar.readonly |
Read-only access |
https://www.googleapis.com/auth/calendar.events |
Read/write events |
https://www.googleapis.com/auth/calendar |
Full access |
Google Drive
| Scope |
Description |
https://www.googleapis.com/auth/drive.file |
App-created/selected files only (only supported) |
https://www.googleapis.com/auth/drive.readonly |
Read all files |
Gmail
| Scope |
Description |
https://www.googleapis.com/auth/gmail.send |
Send emails |
https://www.googleapis.com/auth/gmail.readonly |
Read emails |
https://www.googleapis.com/auth/gmail.modify |
Read/write/send |
Google Sheets
| Scope |
Description |
https://www.googleapis.com/auth/spreadsheets |
Read/write |
https://www.googleapis.com/auth/spreadsheets.readonly |
Read-only |
Google Docs
| Scope |
Description |
https://www.googleapis.com/auth/documents |
Read/write |
https://www.googleapis.com/auth/documents.readonly |
Read-only |
Google Slides
| Scope |
Description |
https://www.googleapis.com/auth/presentations |
Read/write |
https://www.googleapis.com/auth/presentations.readonly |
Read-only |
Slack
| Scope |
Description |
chat:write |
Send messages |
channels:read |
Read channel info |
channels:history, groups:read, im:read, mpim:read |
Read various channels |
files:read, files:write |
File access |
Note: User token integration only. Do not request bot scopes.
Notion
Notion uses a single-scope model - users select pages/databases in the OAuth UI. No scopes needed in JSONC.
Salesforce
| Scope |
Description |
api |
API access |
refresh_token |
Offline access |
HubSpot
| Scope |
Description |
crm.objects.contacts.read/write |
Contacts access |
crm.objects.deals.read/write |
Deals access |
crm.objects.companies.read/write |
Companies access |
LinkedIn
| Scope |
Description |
r_basicprofile, r_profile_basicinfo |
Profile read |
w_member_social |
Post as member |
w_organization_social, r_organization_social |
Org social |
r_organization_admin, rw_organization_admin |
Org admin |
r_ads, r_ads_reporting, rw_ads |
Ads access |
r_verify, r_1st_connections_size |
Verification, connections |
TikTok
| Scope |
Description |
user.info.profile |
Links, bio, verification |
user.info.stats |
Follower/video counts |
video.list |
Read public videos |
artist.certification.read/update |
Artist certification |
Note: Content API and video uploading not supported.
Implementation: Zod Schemas
import { z } from 'zod';
export const IntegrationTypeSchema = z.enum([
'googlecalendar', 'googledrive', 'gmail', 'googlesheets',
'googledocs', 'googleslides', 'slack', 'notion',
'salesforce', 'hubspot', 'linkedin', 'tiktok',
]);
export const ConnectorResourceSchema = z.object({
type: IntegrationTypeSchema,
scopes: z.array(z.string()), // No validation - OAuth provider is the ultimate validator
});
export type ConnectorResource = z.infer<typeof ConnectorResourceSchema>;
Validation Behavior
| Scenario |
Behavior |
| Unknown integration type |
Error: reject file |
| Unknown scope |
No validation - OAuth provider validates during consent |
| Empty scopes array |
Allowed (some integrations like Notion don't need scopes) |
Missing type field |
Error: reject file |
Dependencies
- Apper: New
/sync endpoint - see base44-dev/apper#3374
References