# Send Twenty CRM OAuth Mastery to your agent
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
## Fast path
- Download the package from Yavira.
- Extract it into a folder your agent can access.
- Paste one of the prompts below and point your agent at the extracted folder.
## Suggested prompts
### New install

```text
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete.
```
### Upgrade existing

```text
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run.
```
## Machine-readable fields
```json
{
  "schemaVersion": "1.0",
  "item": {
    "slug": "twenty-oauth-mastery",
    "name": "Twenty CRM OAuth Mastery",
    "source": "tencent",
    "type": "skill",
    "category": "效率提升",
    "sourceUrl": "https://clawhub.ai/avirweb/twenty-oauth-mastery",
    "canonicalUrl": "https://clawhub.ai/avirweb/twenty-oauth-mastery",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadUrl": "/downloads/twenty-oauth-mastery",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=twenty-oauth-mastery",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "packageFormat": "ZIP package",
    "primaryDoc": "SKILL.md",
    "includedAssets": [
      "README.md",
      "SKILL.md"
    ],
    "downloadMode": "redirect",
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/twenty-oauth-mastery"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    }
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/twenty-oauth-mastery",
    "downloadUrl": "https://openagent3.xyz/downloads/twenty-oauth-mastery",
    "agentUrl": "https://openagent3.xyz/skills/twenty-oauth-mastery/agent",
    "manifestUrl": "https://openagent3.xyz/skills/twenty-oauth-mastery/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/twenty-oauth-mastery/agent.md"
  }
}
```
## Documentation

### Twenty CRM OAuth Mastery Skill

Author: Generated from extensive OAuth debugging sessions in OpenCode
Last Updated: 2026-02-08
Version: 1.0

### Skill Metadata

name: twenty-oauth-mastery
description: Expert-level OAuth authentication knowledge for Twenty CRM including implementation, troubleshooting, and best practices
expertise_level: Expert/Mastery
category: Authentication
applicable_to:
  - Twenty CRM authentication
  - Google/Microsoft OAuth
  - Token refresh management
  - Domain restrictions
  - Email/Calendar sync integration
prerequisites:
  - Knowledge of TypeScript/JavaScript
  - Understanding of OAuth 2.0 protocol
  - Familiarity with NestJS framework
keywords:
  - oauth
  - authentication
  - twenty-crm
  - google-oauth
  - microsoft-oauth
  - token-refresh
  - sync-integration
  - domain-restriction

### When to Use This Skill

You should use this skill when working on:

✅ Implementing new OAuth providers
✅ Fixing OAuth login issues
✅ Setting up automatic Gmail/Calendar sync after OAuth
✅ Debugging token refresh failures
✅ Configuring domain restrictions
✅ Troubleshooting redirect loops

### Quick Reference for Common Issues

IssueFile to CheckQuick FixRedirect loopauth.service.tsRebuild: npx nx build twenty-server.co domain blockedgoogle-auth.controller.tsAdd to allowlist: ['company.com', 'company.co']Sync not startinggoogle.auth.strategy.tsReturn tokens in validate()Cookie not readableController cookie settingsSet httpOnly: falseInfinite loopSignInUpGlobalScopeFormEffect.tsxTrack processed token signatures

### 1. Twenty CRM OAuth Architecture

Key Files: twenty/packages/twenty-server/src/engine/core-modules/auth/

Structure:

auth/
├── strategies/         # Passport strategies (Google, Microsoft)
├── controllers/        # OAuth endpoints and callbacks
├── services/          # Auth logic, sync setup, token management
├── guards/            # Auth guards and validation
└── utils/             # Scope configuration, utilities

### 2. Critical Code Patterns

Passport Strategy Pattern (MUST FOLLOW)

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(twentyConfigService: TwentyConfigService) {
    super({
      clientID: twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'),
      clientSecret: twentyConfigService.get('AUTH_GOOGLE_CLIENT_SECRET'),
      callbackURL: twentyConfigService.get('AUTH_GOOGLE_CALLBACK_URL'),
      scope: getGoogleApisOauthScopes(),
      passReqToCallback: true, // 🔴 CRITICAL: Required for request state
    });
  }

  async validate(
    request: GoogleRequest,
    _accessToken: string,
    _refreshToken: string,
    profile: GoogleProfile,
  ) {
    // 🔴 CRITICAL: Include tokens in return object
    // Without this, automatic sync setup fails
    return {
      ...profile,
      accessToken: _accessToken,
      refreshToken: _refreshToken,
      hostedDomain: request.query.hosted_domain || profile.emails?.[0]?.value?.split('@')[1],
    };
  }
}

Why This Matters:

passReqToCallback: true: Enables access to request state
Token preservation: Required for OAuthSyncService to work

### 3. Common Issues & Solutions

Issue 1: Redirect Loop After OAuth

Symptoms: OAuth completes but user stuck on welcome page

Root Causes:

Backend not compiled: Source has fix, container running old JavaScript
Fix:
npx nx build twenty-server
docker restart fratres-twenty



Missing isSingleDomainMode: Redirect logic not in compiled code
Check:
docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode



Cookie domain mismatch: Cookie not accessible
Fix:
// auth.service.ts - Remove explicit domain attribute
res.cookie('tokenPair', JSON.stringify(authTokens), {
  path: '/',
  secure: true,
  sameSite: 'lax',
  httpOnly: false, // 🔴 Must be false for JavaScript access
});

Issue 2: Domain Enforcement Blocking .co Users

Symptoms: @company.co rejected, only @company.com allowed

Three Places to Fix:

Google Strategy (google.auth.strategy.ts):
// ❌ WRONG - Hardcoded
hd: 'company.com'

// ✅ CORRECT - Remove hd parameter
// (no hd parameter)



Controller (google-auth.controller.ts):
// ❌ WRONG - Hardcoded check
if (hostedDomain !== 'company.com') { throw ... }

// ✅ CORRECT - Allowlist
const allowedOAuthDomains = ['company.com', 'company.co'];
if (!hostedDomain || !allowedOAuthDomains.includes(hostedDomain)) {
  throw new UnauthorizedException(
    \`Only ${allowedOAuthDomains.map(d => \`@${d}\`).join(', ')} allowed\`
  );
}



Database (workspaceMetadata table):
INSERT INTO "workspaceMetadata" ("id", "workspaceId", "key", "value", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), 'workspace-id', 'approvedAccessDomains', '["company.com", "company.co"]', NOW(), NOW());

Issue 3: Automatic Sync Not Triggered

Symptoms: User logs in but connected account/sync channels not created

Root Cause: Tokens lost in validate() method

Fix:

// google.auth.strategy.ts validate()
async validate(request, accessToken, refreshToken, profile) {
  // ❌ WRONG - Tokens lost
  return { ...profile };
  
  // ✅ CORRECT - Tokens preserved
  return {
    ...profile,
    accessToken,
    refreshToken,
  };
}

Additional Checks:

Verify auth.service.ts calls oauthSyncService.setupSyncForOAuthUser() after login
Verify tokens are passed to sync service
Check Google scopes include gmail.readonly and calendar.events
Verify CALENDAR_PROVIDER_GOOGLE_ENABLED=true

Issue 4: Frontend Token Processing Loop

Symptoms: SignInUpGlobalScopeFormEffect runs repeatedly, infinite API calls

Root Cause: Same token processed multiple times

Fix:

// SignInUpGlobalScopeFormEffect.tsx
useEffect(() => {
  const tokenPairFromUrl = getAuthPairFromUrl();
  
  if (tokenPairFromUrl) {
    const tokenSignature = JSON.stringify(tokenPairFromUrl);
    
    // 🔴 CRITICAL: Skip if already processed
    if (processedTokenSignatures.current.has(tokenSignature)) {
      return;
    }
    
    // Track this signature
    processedTokenSignatures.current.add(tokenSignature);
    
    // Now process the token
    setAuthTokens(tokenPairFromUrl);
  }
}, []);

### 4. OAuth Sync Integration

When to Use: Users should have Gmail/Calendar auto-connected after OAuth login

Implementation:

Create OAuthSyncService:
async setupSyncForOAuthUser(input: {
  workspaceId: string;
  userId: string;
  workspaceMemberId: string;
  email: string;
  accessToken: string;
  refreshToken: string;
  scopes: string[];
}) {
  // 1. Create/update connected account with tokens
  // 2. Create message channel
  // 3. Create calendar channel (if enabled)
  // 4. Queue initial sync jobs
}



Integrate into AuthService:
// auth.service.ts:signInUpWithSocialSSO()
const { redirectUrl, authTokens } = await this.generateTokens(...);

// 🔴 CRITICAL: Call sync setup BEFORE redirect
if (provider === 'google') {
  try {
    await this.oauthSyncService.setupSyncForOAuthUser({
      workspaceId,
      userId,
      email: user.email,
      accessToken: authTokens.authToken.accessToken,
      refreshToken: authTokens.authToken.refreshToken,
      scopes: user.scopes || [],
    });
  } catch (error) {
    // Log error but don't fail login
    this.logger.error('Failed to setup OAuth sync', error);
  }
}

return { redirectUrl, authTokens };

Critical:

Use try/catch to prevent sync setup from failing login
Check for existing channels (prevent duplication)
Only run for specific providers/domains if needed

### 5. Token Refresh Management

Token Refresh Pattern:

async refreshTokens(refreshToken: string): Promise<ConnectedAccountTokens> {
  const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret);
  oAuth2Client.setCredentials({ refresh_token: refreshToken });
  
  try {
    const { token } = await oAuth2Client.getAccessToken();
    
    // 🔴 CRITICAL: Preserve original refresh token
    // Google may not return a new one
    return {
      accessToken: token,
      refreshToken: refreshToken,
    };
  } catch (error) {
    throw parseGoogleOAuthError(error);
  }
}

Error Handling:

export const parseGoogleOAuthError = (error: unknown) => {
  const gaxiosError = error as GaxiosError;
  const code = gaxiosError.response?.status;
  const reason = gaxiosError.response?.data?.error;
  
  switch (code) {
    case 400:
      if (reason === 'invalid_grant') {
        // 🔴 FATAL: Refresh token expired/revoked
        return new ConnectedAccountRefreshAccessTokenException(
          'invalid_grant',
          ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
        );
      }
      break;
    case 401:
      return new ConnectedAccountRefreshAccessTokenException(
        'unauthorized',
        ConnectedAccountRefreshAccessTokenExceptionCode.UNAUTHORIZED,
      );
    case 429:
      // 🔴 RETRYABLE: Rate limit error
      return new ConnectedAccountRefreshAccessTokenException(
        'rate_limit',
        ConnectedAccountRefreshAccessTokenExceptionCode.RATE_LIMIT_ERROR,
      );
  }
  
  return new ConnectedAccountRefreshAccessTokenException('unknown', ...);
};

### 6. Testing Strategies

Unit Testing (Token Refresh)

describe('GoogleAPIRefreshAccessTokenService', () => {
  it('should refresh token successfully', async () => {
    const mockRefreshToken = 'valid-refresh-token';
    const mockNewAccessToken = 'new-access-token';
    
    jest.spyOn(google.auth, 'OAuth2').mockImplementation(() => ({
      setCredentials: jest.fn(),
      getAccessToken: jest.fn().mockResolvedValue({ token: mockNewAccessToken }),
    }));
    
    const result = await service.refreshTokens(mockRefreshToken);
    
    expect(result.accessToken).toBe(mockNewAccessToken);
    expect(result.refreshToken).toBe(mockRefreshToken); // Original preserved
  });
});

Cookie Injection Test (Playwright)

// Test: frontend reads and processes cookie
await context.addCookies([{
  name: 'tokenPair',
  value: JSON.stringify({ authToken: { accessToken: 'fake-token' } }),
  domain: 'isearch.1791technology.com',
  path: '/',
  secure: true,
  sameSite: 'Lax',
}]);

await page.goto('https://isearch.1791technology.com');

// Check console logs
const logs = await page.evaluate(() => window.tokenPairLogs || []);
assert(logs.includes('tokenPairPayload from cookies: found'));
assert(logs.includes('Setting auth tokens...'));

### 7. Configuration

Required Environment Variables:

# Google OAuth
AUTH_GOOGLE_ENABLED=true
AUTH_GOOGLE_CLIENT_ID=849758856044-54v9md2rt6ucthch26p8g4etotcb8gth.apps.googleusercontent.com
AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
AUTH_GOOGLE_CALLBACK_URL=https://yourdomain.com/auth/google/redirect

# Calendars/Email
CALENDAR_PROVIDER_GOOGLE_ENABLED=true
MESSAGING_PROVIDER_GMAIL_ENABLED=true

# Billing (disable for self-hosted)
IS_BILLING_ENABLED=false

Google Cloud Console:

Redirect URIs: https://yourdomain.com/auth/google/redirect
Authorized Origins: https://yourdomain.com

### 8. Deployment Checklist

Before Deploying:

TypeScript source updated
 Unit tests passing
 Type check: npx nx typecheck twenty-server
 Build: npx nx build twenty-server
 Verify compiled JavaScript has changes (check dist/ folder)
 Copy dist/ to container
 Restart container
 Check health: curl -f /healthz

After Deploying:

Test OAuth flow manually
 Check browser console
 Verify redirect to dashboard (not welcome)
 Check connected account in database
 Verify sync channels created (if applicable)

### 9. Troubleshooting Workflow

Step 1: Verify Container Running New Code

docker ps | grep fratres-twenty
docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode

Step 2: Check Google Cloud Console

Redirect URIs match production URL
Client ID and secret correct
OAuth consent screen configured

Step 3: Check Environment

docker exec fratres-twenty env | grep AUTH_GOOGLE
docker exec fratres-twenty env | grep CALENDAR_PROVIDER

Step 4: Test OAuth Entry Point

curl -v https://yourdomain.com/auth/google | grep Location
# Should redirect to accounts.google.com with correct client_id

Step 5: Check Database (Sync Issues)

-- Check connected accounts
SELECT id, handle, provider, "accessToken" IS NOT NULL
FROM "connectedAccount"
WHERE handle = 'user@example.com';

-- Check sync channels
SELECT id, "syncStatus"
FROM "messageChannel"
WHERE "connectedAccountId" = 'account-id';

Step 6: Check Logs

docker logs fratres-twenty --tail 100 | grep -i oauth

### 10. Common Pitfalls ❌

Forgetting to rebuild - Source changes don't auto-compile
Hardcoding domains - Use allowlists instead
Setting httpOnly: true - Frontend can't read tokenPair cookie
Losing tokens in validate() - Must return accessToken/refreshToken
Not preserving refresh tokens - Google may not return new ones
Missing passReqToCallback: true - Can't access request state
Not testing with real OAuth - Mock tests miss edge cases
Skipping health checks - Container running old code unnoticed

### When OAuth Works But Sync Doesn't

Debug Path:

Check oauth-sync.service.ts exists and is called
Verify tokens passed through validate()
Check scopes include gmail.readonly and calendar.events
Verify CALENDAR_PROVIDER_GOOGLE_ENABLED=true
Check connected account in database
Verify sync channels with syncStatus=ONGOING

Common Fix: Return tokens in validate() method

### When .co Domain Users Can't Login

Debug Path:

Check google.auth.strategy.ts for hardcoded hd parameter
Check google-auth.controller.ts domain validation
Check auth.service.ts domain allowlist
Check workspaceMetadata.approvedAccessDomains in database

Common Fixes:

Remove hardcoded hd parameter
Update controller/service allowlists
Insert domain into database

### When Frontend Gets Stuck on Welcome Page

Debug Path:

Check isSingleDomainMode logic in auth.service.ts
Check compiled auth.service.js has logic
Check computeRedirectURI returns AppPath.Index
Check cookie httpOnly attribute

Common Fixes:

Rebuild backend: npx nx build twenty-server
Ensure redirect to dashboard: AppPath.Index
Set httpOnly: false on cookie

### Quick Commands

# Build backend
npx nx build twenty-server

# Build frontend
npx nx build twenty-front

# Typecheck
npx nx typecheck twenty-server

# Restart container
docker restart fratres-twenty

# Check logs
docker logs fratres-twenty --tail 100

# Health check
curl -f https://yourdomain.com/healthz

# Test OAuth redirect
curl -v https://yourdomain.com/auth/google

### Summary

This skill provides expert-level OAuth knowledge for Twenty CRM covering:

Architecture: Twenty's OAuth using Passport strategies
Common Issues: 5+ major issues with detailed fixes
Automatic Sync: Gmail/Calendar sync after OAuth
Token Management: Refresh patterns and error handling
Testing: Unit and integration test patterns
Configuration: Required environment variables
Deployment: Step-by-step checklist
Troubleshooting: Systematic workflow

Use this skill when:

Implementing new OAuth provider
Fixing OAuth login issues
Setting up automatic sync integration
Debugging token refresh failures
Configuring domain restrictions
Troubleshooting redirect loops
## Trust
- Source: tencent
- Verification: Indexed source record
- Publisher: avirweb
- Version: 1.0.0
## Source health
- Status: healthy
- Source download looks usable.
- Yavira can redirect you to the upstream package for this source.
- Health scope: source
- Reason: direct_download_ok
- Checked at: 2026-04-30T16:55:25.780Z
- Expires at: 2026-05-07T16:55:25.780Z
- Recommended action: Download for OpenClaw
## Links
- [Detail page](https://openagent3.xyz/skills/twenty-oauth-mastery)
- [Send to Agent page](https://openagent3.xyz/skills/twenty-oauth-mastery/agent)
- [JSON manifest](https://openagent3.xyz/skills/twenty-oauth-mastery/agent.json)
- [Markdown brief](https://openagent3.xyz/skills/twenty-oauth-mastery/agent.md)
- [Download page](https://openagent3.xyz/downloads/twenty-oauth-mastery)