launchthat
Building a Monday.com App: OAuth, Pagebuilder, and the Gaps Nobody Warns You About
Monday.com's app framework promises drag-and-drop integration. The reality involves undocumented OAuth quirks, a pagebuilder that fights your layout, and an approval process that rejects you for reasons you cannot reproduce.
Monday.com's developer documentation makes building an app look straightforward. Create an app in the developer center, configure OAuth, embed your frontend in an iframe, submit for review. Four steps. The documentation even has screenshots.
What the documentation does not prepare you for is the 47 things that go wrong between those four steps. I spent three weeks building our Monday.com integration, and roughly two of those weeks were spent on problems the docs never mention.
The OAuth maze
Monday.com uses OAuth 2.0 for authentication. Standard protocol, well-understood flow. Except their implementation has quirks that break standard OAuth libraries.
The redirect URI problem
Most OAuth providers let you register multiple redirect URIs and select one per request. Monday.com requires an exact match — protocol, domain, port, and path. No wildcards. No query parameters.
This means your development environment (localhost:3000), staging (staging.yourdomain.com), and production (yourdomain.com) each need a separate app registration in Monday's developer center. Three apps. Three sets of credentials. Three sets of scopes to keep in sync.
We automated this with environment-specific config files:
const mondayConfig = {
development: {
clientId: process.env.MONDAY_DEV_CLIENT_ID,
redirectUri: "http://localhost:3000/api/monday/callback",
},
staging: {
clientId: process.env.MONDAY_STAGING_CLIENT_ID,
redirectUri: "https://staging.portal.launchthat.com/api/monday/callback",
},
production: {
clientId: process.env.MONDAY_PROD_CLIENT_ID,
redirectUri: "https://portal.launchthat.com/api/monday/callback",
},
};
The token refresh that is not a refresh
Monday.com access tokens expire. The documentation says to use the refresh token to get a new one. What it does not say is that the refresh token is also single-use. If your refresh request fails due to a network issue and you retry with the same refresh token, the retry fails. You now have no valid tokens and the user needs to re-authorize.
We solved this with a lock-and-retry pattern:
async function refreshMondayToken(workspaceId: string): Promise<TokenPair> {
const lock = await acquireLock(`monday-refresh:${workspaceId}`, 10_000);
if (!lock) {
await waitForLockRelease(`monday-refresh:${workspaceId}`);
return getCurrentToken(workspaceId);
}
try {
const current = await getCurrentToken(workspaceId);
const response = await monday.refreshToken(current.refreshToken);
await storeToken(workspaceId, response);
return response;
} finally {
await releaseLock(lock);
}
}
Only one refresh attempt can happen at a time per workspace. If a concurrent request needs a fresh token while a refresh is in progress, it waits for the first refresh to complete and uses that result.
The pagebuilder reality
Monday.com apps can embed UI inside Monday's interface using their pagebuilder. Your app renders in an iframe within a Monday board view. The promise is seamless integration. The reality is a constant negotiation with the iframe boundary.
Iframe constraints
Your app runs inside a fixed-size iframe. You do not control the dimensions. Monday sets them based on the view type (board view, item view, dashboard widget). Responsive design is not optional — it is survival.
function useMondayViewport() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
});
observer.observe(document.documentElement);
return () => observer.disconnect();
}, []);
return size;
}
We measure the available space on every resize and adjust our layout accordingly. A dashboard widget might give you 300x200 pixels. A board view might give you the full viewport. Your UI needs to work in both.
Communication across the iframe boundary
Monday provides a client SDK (monday-sdk-js) for communication between your app and the Monday.com shell. It handles:
- Context: which board, item, or view the user is looking at
- Settings: user-configured options for your app instance
- Storage: key-value storage scoped to your app
- API calls: authenticated GraphQL queries through Monday's proxy
The SDK is event-based. Context changes do not trigger re-renders — you listen for events and update state manually:
function useMondayContext() {
const [context, setContext] = useState<MondayContext | null>(null);
useEffect(() => {
monday.listen("context", (res: { data: MondayContext }) => {
setContext(res.data);
});
monday.get("context").then((res: { data: MondayContext }) => {
setContext(res.data);
});
}, []);
return context;
}
The approval process
After building the integration, you submit it for review. Monday's team tests your app and either approves it or sends it back with feedback.
The feedback is where things get interesting. We were rejected three times:
Rejection 1: "App does not load in board view." We could not reproduce this. It loaded in every browser we tested. After two days of debugging, we discovered that Monday's review environment uses a specific content security policy that blocked one of our CDN-hosted fonts. The font failure caused a CSS layout break that made the app appear empty.
Rejection 2: "OAuth flow does not complete." Our redirect URI was configured for our production domain. The reviewer was testing from Monday's internal staging environment, which sent a different state parameter format than production. Our state validation rejected it.
Rejection 3: "App does not handle workspace with no boards." Fair point. We had not tested the empty state because all of our test workspaces had boards. Added a proper empty state with a "Create your first board" prompt.
Each rejection added 3-5 business days to the review cycle. The total time from first submission to approval was six weeks.
What we built to make this sustainable
We automated everything that burned us:
Environment management
A CLI tool that creates and syncs Monday.com app registrations across environments. Change a scope in one place, it propagates everywhere.
Token management
All Monday.com tokens are stored in Convex with automatic refresh, lock-based concurrency control, and alerting when tokens cannot be refreshed.
Integration testing
A test suite that runs our app inside a simulated Monday.com iframe environment with various viewport sizes, context payloads, and CSP configurations. We catch the issues that caused our rejections before we submit.
Error reporting
Every Monday.com API error is logged with the full request context, response headers, and rate limit status. When something breaks in production, we can reproduce it from the log entry alone.
Advice for teams building Monday.com apps
Start with the OAuth flow and test it in all three environments before building any UI. The OAuth quirks will cost you days if you discover them late.
Build your UI outside the iframe first. Get the components working in a standard browser context. Then adapt for iframe constraints. Debugging layout issues inside an iframe is significantly harder than debugging them in a standalone page.
Read the community forums, not just the docs. The official documentation covers the happy path. The forums cover everything else. We found solutions to 4 of our 5 hardest problems in forum threads.
Budget six weeks for the approval process. Even if your app is perfect on first submission — which it will not be — the review cycle takes time. Plan your launch timeline accordingly.
The Monday.com integration is now one of the most-used features in our portal. Users love it. Getting there was not hard because the API was bad — it was hard because the gap between documentation and reality was wider than expected. Every integration platform has this gap. Monday.com's is just particularly well-hidden.
Want to see how this was built?
See the portal projectWant to see how this was built?
Browse all posts