Twenty CRM OAuth Mastery Skill
**Author**: Generated from extensive OAuth debugging sessions in OpenCode
Best use case
Twenty CRM OAuth Mastery Skill is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
**Author**: Generated from extensive OAuth debugging sessions in OpenCode
Teams using Twenty CRM OAuth Mastery Skill should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/twenty-oauth-mastery/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How Twenty CRM OAuth Mastery Skill Compares
| Feature / Agent | Twenty CRM OAuth Mastery Skill | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
**Author**: Generated from extensive OAuth debugging sessions in OpenCode
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
AI Agents for Marketing
Discover AI agents for marketing workflows, from SEO and content production to campaign research, outreach, and analytics.
AI Agents for Coding
Browse AI agent skills for coding, debugging, testing, refactoring, code review, and developer workflows across Claude, Cursor, and Codex.
Cursor vs Codex for AI Workflows
Compare Cursor and Codex for AI coding workflows, repository assistance, debugging, refactoring, and reusable developer skills.
SKILL.md Source
# Twenty CRM OAuth Mastery Skill
**Author**: Generated from extensive OAuth debugging sessions in OpenCode
**Last Updated**: 2026-02-08
**Version**: 1.0
---
## Skill Metadata
```yaml
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
```
---
## Quick Start
### 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
| Issue | File to Check | Quick Fix |
|-------|---------------|-----------|
| Redirect loop | `auth.service.ts` | Rebuild: `npx nx build twenty-server` |
| .co domain blocked | `google-auth.controller.ts` | Add to allowlist: `['company.com', 'company.co']` |
| Sync not starting | `google.auth.strategy.ts` | Return tokens in validate() |
| Cookie not readable | Controller cookie settings | Set `httpOnly: false` |
| Infinite loop | `SignInUpGlobalScopeFormEffect.tsx` | Track processed token signatures |
---
## Core Knowledge
### 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)
```typescript
@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**:
1. **Backend not compiled**: Source has fix, container running old JavaScript
**Fix**:
```bash
npx nx build twenty-server
docker restart fratres-twenty
```
2. **Missing isSingleDomainMode**: Redirect logic not in compiled code
**Check**:
```bash
docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode
```
3. **Cookie domain mismatch**: Cookie not accessible
**Fix**:
```typescript
// 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**:
1. **Google Strategy** (`google.auth.strategy.ts`):
```typescript
// ❌ WRONG - Hardcoded
hd: 'company.com'
// ✅ CORRECT - Remove hd parameter
// (no hd parameter)
```
2. **Controller** (`google-auth.controller.ts`):
```typescript
// ❌ 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`
);
}
```
3. **Database** (`workspaceMetadata` table):
```sql
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**:
```typescript
// 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**:
1. Verify `auth.service.ts` calls `oauthSyncService.setupSyncForOAuthUser()` after login
2. Verify tokens are passed to sync service
3. Check Google scopes include `gmail.readonly` and `calendar.events`
4. 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**:
```typescript
// 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**:
1. **Create OAuthSyncService**:
```typescript
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
}
```
2. **Integrate into AuthService**:
```typescript
// 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**:
```typescript
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**:
```typescript
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)
```typescript
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)
```typescript
// 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**:
```bash
# 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**
```bash
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**
```bash
docker exec fratres-twenty env | grep AUTH_GOOGLE
docker exec fratres-twenty env | grep CALENDAR_PROVIDER
```
**Step 4: Test OAuth Entry Point**
```bash
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)**
```sql
-- 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**
```bash
docker logs fratres-twenty --tail 100 | grep -i oauth
```
---
### 10. Common Pitfalls ❌
1. **Forgetting to rebuild** - Source changes don't auto-compile
2. **Hardcoding domains** - Use allowlists instead
3. **Setting httpOnly: true** - Frontend can't read tokenPair cookie
4. **Losing tokens in validate()** - Must return accessToken/refreshToken
5. **Not preserving refresh tokens** - Google may not return new ones
6. **Missing passReqToCallback: true** - Can't access request state
7. **Not testing with real OAuth** - Mock tests miss edge cases
8. **Skipping health checks** - Container running old code unnoticed
---
## Expert Insights
### When OAuth Works But Sync Doesn't
**Debug Path**:
1. Check `oauth-sync.service.ts` exists and is called
2. Verify tokens passed through validate()
3. Check scopes include `gmail.readonly` and `calendar.events`
4. Verify `CALENDAR_PROVIDER_GOOGLE_ENABLED=true`
5. Check connected account in database
6. Verify sync channels with `syncStatus=ONGOING`
**Common Fix**: Return tokens in validate() method
---
### When .co Domain Users Can't Login
**Debug Path**:
1. Check `google.auth.strategy.ts` for hardcoded `hd` parameter
2. Check `google-auth.controller.ts` domain validation
3. Check `auth.service.ts` domain allowlist
4. 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**:
1. Check `isSingleDomainMode` logic in `auth.service.ts`
2. Check compiled `auth.service.js` has logic
3. Check `computeRedirectURI` returns `AppPath.Index`
4. 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
```bash
# 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:
1. **Architecture**: Twenty's OAuth using Passport strategies
2. **Common Issues**: 5+ major issues with detailed fixes
3. **Automatic Sync**: Gmail/Calendar sync after OAuth
4. **Token Management**: Refresh patterns and error handling
5. **Testing**: Unit and integration test patterns
6. **Configuration**: Required environment variables
7. **Deployment**: Step-by-step checklist
8. **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 loopsRelated Skills
Presentation Mastery — Complete Slide Design & Delivery System
You are a Presentation Architect. You help build presentations that persuade, inform, and move people to action. You cover the full lifecycle: audience analysis → narrative structure → slide design → delivery coaching → post-presentation follow-up.
Photography Mastery
Complete photography system — exposure, composition, lighting, genre-specific workflows, editing, gear selection, portfolio building, and client management. From beginner to professional.
OpenClaw Mastery — The Complete Agent Engineering & Operations System
> Built by AfrexAI — the team that runs 9+ production agents 24/7 on OpenClaw.
Negotiation Mastery
Complete negotiation system for business deals, salary talks, vendor contracts, partnerships, and high-stakes conversations. Combines multiple proven frameworks (FBI tactical empathy, Harvard principled negotiation, SPIN, anchoring science) into one actionable playbook.
n8n Workflow Mastery — Complete Automation Engineering System
You are an expert n8n workflow architect. You design, build, debug, optimize, and scale n8n automations following production-grade methodology. Every workflow you create is complete, functional, and follows the patterns in this guide.
Meeting Mastery — AI Meeting Prep, Notes & Follow-Up Engine
You are an elite meeting preparation and follow-up agent. You ensure every meeting is high-value — thoroughly prepared beforehand, cleanly documented during, and actioned after.
Growth Engineering Mastery
> Complete growth system: experimentation engine, viral mechanics, channel playbooks, funnel optimization, retention loops, and scaling frameworks. From zero users to exponential growth.
Database Engineering Mastery
> Complete database design, optimization, migration, and operations system. From schema design to production monitoring — covers PostgreSQL, MySQL, SQLite, and general SQL patterns.
afrexai-copywriting-mastery
Write high-converting copy for any medium — landing pages, emails, ads, UX, sales pages, video scripts, and brand voice. Complete methodology with frameworks, templates, scoring rubrics, and swipe files. Use when writing or reviewing any user-facing text.
pinescript-mastery
Transforms the AI into an elite Pine Script developer and trading strategy architect — capable of writing production-grade indicators, strategies, and libraries from scratch, debugging complex scripts, designing complete trading systems, and teaching Pine Script at any level. Covers Pine Script v5 and the latest v6 (December 2024) with all 2025 updates: unlimited local scopes, 10x string limits, dynamic request.*() calls, strict boolean logic, and the full modern feature set. Goes beyond code: designs multi-factor strategy logic, builds risk management systems, optimizes for backtesting accuracy, prevents overfitting, and produces TradingView-publishable scripts with professional structure, documentation, and UX. Trigger on: "write a Pine Script", "create an indicator", "build a strategy in Pine Script", "Pine Script help", "TradingView script", "debug my Pine Script", "how do I code X in Pine Script", "convert this to Pine Script", "optimize my strategy", "Pine Script v5", "Pine Script v6", or any request involving TradingView indicator or strategy development. Also trigger when the user describes a trading idea and wants it coded, even without explicitly mentioning Pine Script.
Technical SEO Mastery
> Complete technical SEO audit, fix, and monitoring system. From crawlability to Core Web Vitals to international SEO — everything search engines need to find, crawl, index, and rank your site.
Stakeholder Management Mastery
You are a stakeholder management strategist. You help identify, analyze, engage, and manage stakeholders across any project, initiative, or organizational change to maximize alignment, minimize resistance, and drive successful outcomes.