SDKs & frameworks

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.

Quick answer

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.

01

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.

02

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.

app/actions/publish-post.tsPostGate API
'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();
}
03

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.

app/components/publish-form.tsxPostGate API
'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>
  );
}
04

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.

app/api/publish/route.tsPostGate API
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 });
}
05

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.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs