StarlingPost is a multi-platform social media scheduling and automation tool built on Next.js 16 (App Router), Firebase, and Vercel. This post covers the architecture decisions that shaped it, including several we would make differently if we started today.
Framework: Next.js App Router
We chose Next.js 16 with the App Router over a separate backend because the deployment model on Vercel makes API routes a first-class citizen. OAuth callback routes, publish endpoints, cron triggers, and webhook handlers all live in app/api/alongside the frontend. There is no separate server to deploy or maintain.
The tradeoff: API routes in Next.js run as serverless functions, which means cold starts. For endpoints that handle OAuth callbacks (user is actively waiting), cold starts are noticeable. For cron endpoints, they do not matter. We accepted this tradeoff because it simplified the deployment stack significantly.
React 19 is paired with the App Router. Server Components handle data fetching and rendering for SEO-critical pages; Client Components handle interactivity (the post composer, automation rule editor, etc.). The split is mostly clean with a few places where we had to lift state or wrap client UIs in server shells.
Database and auth: Firebase
Firebase Auth handles user identity. We chose it primarily because it provides OAuth sign-in providers out of the box and handles token refresh automatically. The alternative — building auth on top of NextAuth.js or a custom JWT flow — would have required significantly more implementation work.
Firestore stores everything: user profiles, connected platform accounts (with OAuth tokens), scheduled posts, automation rules, and dedup tracking for auto-replies. The schema for connected accounts is an array on the user document:
users/{uid}
connectedAccounts[]: {
platform, platformId, accountName,
accessToken, refreshToken,
oauthToken, oauthTokenSecret // Twitter OAuth 1.0a
}This works well for querying all accounts for a user in a single read. The downside: if a user connects 20 accounts across platforms, the user document grows large. For the scale we are targeting (solo creators and small agencies), this is not a problem in practice.
The Twitter OAuth nightmare
Twitter/X requires two separate OAuth flows depending on what you are doing:
- OAuth 2.0 (PKCE): Required for the v2 API endpoints — reading tweets, user lookup, analytics. Access tokens are short-lived and require refresh.
- OAuth 1.0a: Required for the v1.1 API endpoints — specifically for posting tweets via
twitter-api-v2. UsesoauthTokenandoauthTokenSecret, which do not expire.
We ended up maintaining two separate auth flows in the codebase: one for OAuth 2.0 (reading, analytics) and one for OAuth 1.0a (posting). Both sets of tokens live on the same connected account record. This is inelegant but unavoidable — Twitter's API tier restrictions force the split.
YouTube integration
YouTube uses the Google OAuth 2.0 flow via googleapis. The YouTube Data API v3 handles channel info, video lists, analytics, and comment management. The scope requirements are specific: youtube.readonly for data access, youtube.force-ssl for write operations.
Video upload is stubbed — the architecture supports it but media handling is pending. The file size constraints for direct upload via the API versus a resumable upload session require separate handling that we have not yet built.
Automation: cron over webhooks
Comment automation runs on a daily cron job via Vercel Scheduled Functions. We chose polling over webhooks deliberately:
- Webhooks require each platform to call a publicly accessible endpoint — which means authentication, signature verification, and handling platform-specific payload formats for six different APIs.
- Cron polling is simpler to implement, debug, and scale. The tradeoff is latency: replies go out on a schedule, not in real time. For the use cases we target (keyword auto-reply, templated responses), a daily cadence is acceptable.
AI: OpenAI gpt-4o-mini
We chose gpt-4o-mini over gpt-4o for cost. Caption enhancement runs per-post, per-platform — at scale, the per-token cost difference is significant. The quality difference for short-form copywriting tasks (caption rewriting, hashtag generation, CTA suggestions) is minimal.
Each platform has its own system prompt specifying length constraints, tone, hashtag style, and what to include or avoid. The AI call returns enhanced text, hashtag suggestions, and a CTA, all in a single completion.
What we would change
If we were starting fresh today, we would use a separate jobs queue (something like Upstash QStash or BullMQ on a long-running instance) instead of Vercel cron. Serverless cron functions have execution time limits that constrain how much processing we can do per run. A queue-based approach would let us spread work across many small function invocations with proper retry logic and progress tracking.
We would also separate the connected account tokens into their own Firestore subcollection rather than embedding them in the user document — cleaner access patterns and better security boundary (token reads do not require reading the full user profile).