Adding Social Media Publishing to Your Next.js SaaS: The API-First Approach
Most SaaS teams underestimate what it takes to build social media features themselves. Seven OAuth flows, platform-specific rate limits, and diverging content APIs — or one integration that handles all of it.
At some point in the life of most SaaS products, someone on the team says: "We should let users post from inside the app." It sounds like a two-day ticket. It is rarely a two-day ticket.
Social media integration is one of those features that looks straightforward from the outside and reveals its full complexity only once you are deep in the OAuth documentation for your third platform. This post covers the architectural reality of building social publishing into a Next.js SaaS, the specific failure modes of the DIY approach, and when a unified social API makes the engineering trade-off obvious.
Why SaaS Products Add Social Features
Before the implementation, it is worth being precise about what social features actually do for a product — because the answer shapes how you build them.
User value: Many SaaS workflows produce outputs users want to share. A design tool produces graphics. A reporting tool produces charts. An analytics platform produces metrics updates. Social publishing closes the loop: users can share results without leaving the product, which increases perceived value and session depth.
Viral loop: Every post a user publishes from your product is a brand impression for your product. If the post carries attribution — even just "via [Your Product]" in a scheduling interface — every piece of content becomes a distribution channel. This is the same mechanic behind Canva's watermarks and Buffer's default tweet attribution.
Retention: Features that connect to a user's existing workflow reduce churn. A user who posts to LinkedIn every week via your SaaS is harder to replace than a user who only logs in to run a report.
These are real business reasons to build the feature. The engineering question is whether you build it layer by layer against each platform's API, or you drop in a single integration that abstracts the platform differences away.
The DIY Reality: What You Are Actually Building
Let's be concrete about what "build it ourselves" means when you want to support Instagram, LinkedIn, TikTok, and Twitter/X.
OAuth for each platform — separately
Every social platform has its own OAuth 2.0 implementation, and they are not interchangeable. You need to:
- Register an app on each platform's developer portal
- Handle each platform's specific scopes (LinkedIn uses
w_member_social, Instagram usesinstagram_basic,instagram_content_publish, and so on) - Implement the authorization flow, store access tokens securely, and handle token refresh — each platform has different token lifetimes and refresh mechanics
- Manage webhook verification — some platforms sign payloads with HMAC-SHA256, others use different schemes, some don't sign at all
Here is what a minimal OAuth callback for a single platform looks like in Next.js:
// app/api/oauth/linkedin/callback/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
// Validate state to prevent CSRF
const storedState = await getOAuthState(state)
if (!storedState) return new Response('Invalid state', { status: 400 })
// Exchange code for access token
const tokenResponse = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: process.env.LINKEDIN_REDIRECT_URI!,
client_id: process.env.LINKEDIN_CLIENT_ID!,
client_secret: process.env.LINKEDIN_CLIENT_SECRET!,
}),
})
const { access_token, expires_in, refresh_token } = await tokenResponse.json()
// LinkedIn tokens expire in 60 days — store expiry for refresh logic
await storeToken({
userId: storedState.userId,
platform: 'linkedin',
accessToken: access_token,
expiresAt: Date.now() + expires_in * 1000,
refreshToken: refresh_token,
})
return Response.redirect('/settings/connections')
}
Multiply this by seven platforms. Each with different token endpoints, different scope strings, different expiry windows, different refresh mechanisms. Instagram requires a separate business account connection step. TikTok has a different auth server for different regions. Reddit uses bearer tokens with different scope requirements for posting vs. reading.
This is before you have written a single line of code that actually posts content.
Platform-specific posting APIs
Once you have tokens, the posting APIs are not uniform:
- LinkedIn posts use a structured JSON body with
author,specificContent, andvisibilitykeys - Instagram requires a two-step process: create a media container, then publish it (with a delay between steps)
- TikTok's posting API requires video uploads via a chunked upload endpoint, not a simple URL reference
- Reddit posts differ between text posts, link posts, and image posts — different endpoints, different required fields
A naive abstraction that tries to paper over these differences grows into a significant maintenance surface. When LinkedIn updates their API (which they do, roughly annually), your abstraction breaks. When TikTok adds a new content type, your abstraction does not support it until you update it.
Rate limits and error handling per platform
Each platform has its own rate limit model:
- LinkedIn: 100 requests per day per app for posting
- Instagram: per-user and per-app limits, with different buckets for different endpoint types
- TikTok: per-user daily posting limits that vary by account tier
A robust implementation tracks remaining limits, backs off correctly when limits are hit, and surfaces actionable errors to users — not raw API error codes that mean nothing to a non-developer.
Building this correctly for four platforms is a meaningful engineering project. Building it for seven, maintaining it, and expanding it as your platform list grows is an ongoing infrastructure commitment.
The API-First Alternative
A unified social API abstracts all of the above — OAuth, token management, platform differences, rate limits — behind a single integration point. You integrate once and get access to every platform the provider supports.
Aether is the integration we reach for in this category. A single API key, seven platforms, and a set of SDKs that cover TypeScript, Python, and others. Here is what posting to multiple platforms simultaneously looks like:
// lib/social.ts
import { AetherClient } from '@aetherhq/sdk'
const aether = new AetherClient({ apiKey: process.env.AETHER_API_KEY! })
// Post to LinkedIn and Twitter simultaneously
const result = await aether.publish({
accounts: ['linkedin_account_id', 'twitter_account_id'],
content: {
text: 'Excited to share our Q2 results — 3x growth in active users.',
},
// Per-platform overrides when content needs to differ
overrides: {
linkedin: {
text: 'Excited to share our Q2 results — 3x growth in active users. Full breakdown in the comments.',
},
},
})
The same call handles token management, rate limit awareness, and platform-specific payload formatting. If LinkedIn requires a structured body and Twitter wants plain text, the SDK handles the translation.
Scheduling
Scheduled posting — one of the most-requested features in social integration — is a single parameter:
await aether.publish({
accounts: ['instagram_account_id'],
content: {
text: 'Our weekly metrics update.',
mediaUrls: ['https://cdn.yourapp.com/charts/weekly-q2.png'],
},
scheduledAt: new Date('2026-07-01T09:00:00Z'), // timezone-aware
})
Building this yourself requires a job queue, a cron scheduler, state management for scheduled posts, and retry logic for failed deliveries. The Aether SDK makes it a field on the publish call.
The OAuth connection flow: Magic Connect Links
Connecting a user's social account — the OAuth step — is handled via a hosted connection link:
// app/api/social/connect/route.ts
import { AetherClient } from '@aetherhq/sdk'
const aether = new AetherClient({ apiKey: process.env.AETHER_API_KEY! })
export async function POST(request: Request) {
const { platform, userId } = await request.json()
const { connectUrl } = await aether.accounts.createConnectLink({
platform,
externalUserId: userId, // ties the connected account to your user
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/connections`,
})
return Response.json({ connectUrl })
}
The user visits the connect URL, completes OAuth on Aether's hosted flow, and lands back in your app with the account connected. No OAuth callback handlers, no token storage, no platform-specific scope management in your codebase.
Analytics
Once accounts are connected and posts are going out, analytics come back through the same API:
const metrics = await aether.analytics.getPostMetrics({
postId: 'post_abc123',
})
// Returns normalized metrics across platforms
// { impressions: 4820, engagements: 312, clicks: 89, shares: 23 }
Normalized metrics across platforms are the detail that makes unified APIs genuinely valuable for analytics. Comparing LinkedIn impressions to TikTok views to Twitter reach is apples-to-oranges natively — each platform defines and counts these differently. A unified layer that normalizes to a common schema is real engineering work.
When to DIY vs. When to Reach for a Unified API
The honest answer: most SaaS teams should not build social integrations from scratch.
Build it yourself when:
- You need a single platform only and that platform is your core product (a Twitter-native analytics tool, for example)
- You have specific compliance requirements that prohibit third-party token management
- You are operating at a scale where the unified API cost is significant relative to engineering cost
Use a unified API when:
- You need two or more platforms
- Social features are supporting functionality, not your core product
- Your team's time is better spent on your actual differentiation
- You want to add platforms in the future without dedicated engineering work
The platform count is the clearest signal. A single platform integration is manageable. Two platforms doubles the OAuth surface, the rate limit handling, and the error cases. Three platforms is where most teams realize they are building infrastructure instead of product.
Adding Social Features to Your Next.js Architecture
If you are wiring this into an existing Next.js App Router application, the integration pattern is straightforward:
// app/api/social/publish/route.ts
import { AetherClient } from '@aetherhq/sdk'
import { getUser } from '@/lib/auth'
import { getUserSocialAccounts } from '@/lib/db'
const aether = new AetherClient({ apiKey: process.env.AETHER_API_KEY! })
export async function POST(request: Request) {
const user = await getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
const { content, platforms, scheduledAt } = await request.json()
// Fetch which Aether account IDs this user has connected
const accounts = await getUserSocialAccounts(user.id, platforms)
const result = await aether.publish({
accounts: accounts.map(a => a.aetherAccountId),
content,
scheduledAt: scheduledAt ? new Date(scheduledAt) : undefined,
})
return Response.json({ success: true, postIds: result.postIds })
}
The surface area in your codebase is minimal. The complexity lives in Aether's infrastructure, not yours.
The Build vs. Buy Calculus
A rough estimate for building multi-platform social integration from scratch:
- OAuth for 4 platforms: 3–5 days
- Posting API implementation per platform: 1–2 days each
- Scheduling infrastructure (queue, cron, retries): 3–4 days
- Rate limit handling: 2–3 days
- Analytics normalization: 3–4 days
- Ongoing maintenance per platform API update: 1–2 days per update, several times per year
At four platforms, you are looking at 4–6 weeks of initial build and a meaningful ongoing maintenance burden. A unified API turns that into an afternoon.
The question is never "can we build it?" — you can. The question is whether building it is the best use of your engineering capacity relative to what your product actually needs next.
Social features are a strong product addition. Building the social plumbing yourself is rarely the right choice when the infrastructure exists to skip it.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources