Social Publishing from Next.js
How to call the PostGate API from a Next.js Server Action or Route Handler while keeping the API key server-side only.
In Next.js, call PostGate only from server-only code — a Server Action ('use server') or a Route Handler (app/api/*/route.ts) — reading POSTGATE_API_KEY from an unprefixed environment variable so it never reaches the browser. Send POST /api/posts with a stable Idempotency-Key, then confirm delivery by polling the post or subscribing to webhooks.
The one rule that matters more than any code sample here: a PostGate API key can create, schedule, and delete content on a customer's connected social accounts, so it must never reach the browser. In the Next.js App Router, that means calling PostGate only from a Server Action or a Route Handler — server-only code that runs on your infrastructure, never in the client bundle.
Step 1 — Why does this have to be server-side?
Next.js ships any environment variable prefixed NEXT_PUBLIC_ straight into the JavaScript your customer's browser downloads. A regular, unprefixed variable like POSTGATE_API_KEY stays on the server — but only as long as you read it from server-only code. Reading it inside a component marked 'use client', or passing it as a prop into one, defeats the boundary just as completely as prefixing it. Server Actions ('use server') and Route Handlers (app/api/*/route.ts) are the two places in the App Router guaranteed to execute only on the server.
Step 2 — Publish from a Server Action
A Server Action is the simplest option when a form on your page should trigger the post — no separate API route to write. Mark the module (or the function) 'use server', read the key inside it, and call POST /api/posts with a stable Idempotency-Key so a resubmitted form never publishes twice.
'use server';
const API_URL = "https://postgate.studio";
export async function publishPost(formData: FormData) {
const apiKey = process.env.POSTGATE_API_KEY;
if (!apiKey) throw new Error("POSTGATE_API_KEY is not configured");
const body = String(formData.get("body") ?? "");
const profileId = String(formData.get("profileId") ?? "");
const response = await fetch(`${API_URL}/api/posts`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `web-form-${profileId}-${Date.now()}`,
},
body: JSON.stringify({
post: { body },
profiles: [profileId],
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
return response.json();
}Step 3 — Call the action from a form
The client component only ever imports the action and passes it to a <form>. It never sees POSTGATE_API_KEY — that identifier does not exist anywhere in the code the browser downloads.
'use client';
import { publishPost } from '../actions/publish-post';
export function PublishForm({ profileId }: { profileId: string }) {
return (
<form action={publishPost}>
<input type="hidden" name="profileId" value={profileId} />
<textarea name="body" required />
<button type="submit">Publish</button>
</form>
);
}Step 4 — Or expose a Route Handler instead
Prefer a Route Handler when a non-form client — a mobile app, a background job, an external webhook consumer — needs to trigger a publish over HTTP. The handler still runs only on the server; your own frontend calls /api/publish on your own domain, and that request never carries the PostGate key itself.
import { NextRequest, NextResponse } from 'next/server';
const API_URL = "https://postgate.studio";
export async function POST(request: NextRequest) {
const apiKey = process.env.POSTGATE_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: 'not_configured' }, { status: 500 });
}
const { body, profileId } = await request.json();
const upstream = await fetch(`${API_URL}/api/posts`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `route-${profileId}-${Date.now()}`,
},
body: JSON.stringify({ post: { body }, profiles: [profileId] }),
});
const data = await upstream.json();
return NextResponse.json(data, { status: upstream.status });
}Step 5 — Confirm delivery and surface failures
Either approach returns acceptance, not proof of publication — read the delivery lifecycle. For a page that needs live status, poll GET /api/posts/{id} from a Server Component or another Route Handler, or subscribe to webhooks and update your own database, which your UI can then read without calling PostGate directly at all. A failed target returns a structured, retryable-flagged error — see errors & retries — that your Server Action or Route Handler should relay to the client as a normal error, not as a generic 500.
Frequently asked questions
Why can't I call the PostGate API from client-side Next.js code?
A PostGate API key can create, schedule, and delete content on a customer's connected social accounts, so it must never reach the browser. Next.js ships any NEXT_PUBLIC_-prefixed variable into the client bundle, and reading an unprefixed variable inside a 'use client' component — or passing it as a prop into one — defeats the server boundary just as completely.
Should I publish from a Server Action or a Route Handler?
Use a Server Action when a form on your page should trigger the post — there is no separate API route to write. Prefer a Route Handler when a non-form client — a mobile app, a background job, an external webhook consumer — needs to trigger a publish over HTTP. Both run only on the server, and your frontend's request never carries the PostGate key itself.
How do I stop a resubmitted form from publishing the same post twice?
Send a stable Idempotency-Key header with the POST /api/posts call inside your Server Action. With the same key, a resubmitted form never publishes twice — the retry maps onto the original request instead of creating a duplicate post.
How do I show live publish status in a Next.js UI?
The create call returns acceptance, not proof of publication. Poll GET /api/posts/{id} from a Server Component or another Route Handler, or subscribe to webhooks and update your own database, which your UI can then read without calling PostGate directly at all. Relay a failed target's structured, retryable-flagged error to the client as a normal error, not a generic 500.
