Next.js

The FoPost TypeScript client wired into Next.js, with a server-only client, Server Action helpers, and cache tags.

@fopost/next is a thin wrapper over the TypeScript SDK. Every request, retry, model, and error class lives there; this package only wires FoPost into Next.js idioms: a server-only client, a webhook Route Handler, Server Action helpers, and cache tags.

Needs Node 20 or newer, Next 14 or 15, and React 18.2 or newer. Ships ESM and CommonJS builds with TypeScript types.

npm install @fopost/next

This is a 0.x release. The surface is still settling and a minor version may break something. Pin an exact version if that matters to you.

Published on npm. Source and issues: github.com/fopost/fopost-next. MIT licensed.

The server-only guarantee

A FoPost API key must never reach the browser, and this package enforces that rather than documenting it.

  • The module that constructs the client imports server-only. Bundling @fopost/next into a Client Component is a build error, not a runtime surprise.
  • The key is read from FOPOST_API_KEY. There is no NEXT_PUBLIC_* path to it, and getFoPostClient() will not fall back to one.
  • Call it from a Server Component, a Route Handler, or a Server Action. Pass data to Client Components, never the client.

If you see This module cannot be imported from a Client Component module, a 'use client' file is importing @fopost/next. Move the call into a Server Action and import that instead.

Environment

VariableRequiredWhat it does
FOPOST_API_KEYyesYour API key, sent as X-API-Key. Server side only
FOPOST_BASE_URLnoOverride the API base URL
FOPOST_WEBHOOK_SECRETfor webhooksSigning secret of the webhook you created in the dashboard

Create a key at Settings → API Keys in the dashboard.

Quick start

// app/posts/page.tsx
import { getFoPostClient } from '@fopost/next';

export default async function PostsPage() {
  const fopost = getFoPostClient();
  const posts = await fopost.posts.list({ workspaceId: process.env.FOPOST_WORKSPACE_ID! });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          {post.title ?? post.id}: {post.status}
        </li>
      ))}
    </ul>
  );
}

getFoPostClient() is memoized per resolved credentials, so calling it in every component and action is free.

Server Actions

Action return values are serialized across the RSC boundary, so an SDK error instance cannot be thrown through one. Every helper returns a discriminated result with a plain-object error instead.

// app/posts/actions.ts
'use server';

import { createPostAction, publishPostAction, foPostTags } from '@fopost/next';

const revalidate = { tags: [foPostTags.posts(WORKSPACE_ID)], paths: ['/posts'] };

const createPost = createPostAction({ revalidate });
const publishPost = publishPostAction({ revalidate });

export async function composeAndPublish(formData: FormData) {
  const created = await createPost({
    workspaceId: WORKSPACE_ID,
    content: [{ text: String(formData.get('text')) }],
    accounts: formData.getAll('accounts').map(String),
  });
  if (!created.ok) return created; // { ok: false, error: { message, status, code } }

  return publishPost(created.data.id);
}
// app/posts/compose-form.tsx
'use client';
import { composeAndPublish } from './actions';

// The action is imported; the client and the key are not.
<form action={composeAndPublish}>...</form>;
HelperWraps
createPostAction()posts.create
updatePostAction()posts.update
publishPostAction()posts.publish
deletePostAction()posts.delete
createFoPostAction(fn)anything else in the SDK

Publishing is create then publish, and publish resolves when delivery is queued, not when the post is live. The post.published webhook tells you it landed.

Webhooks

The API signs the exact bytes it sends:

X-FoPost-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed by your webhook secret>
X-FoPost-Event:     post.published
X-FoPost-Delivery:  <delivery id>

The handler reads the raw body before parsing it, verifies with a constant-time compare, and answers 401 on a mismatch. Re-serializing the parsed JSON would change the bytes and break verification, which is exactly the mistake this helper removes.

// app/api/fopost/webhook/route.ts
import { createFoPostWebhookHandler } from '@fopost/next';

export const runtime = 'nodejs'; // signature verification uses node:crypto

export const POST = createFoPostWebhookHandler({
  // secret defaults to process.env.FOPOST_WEBHOOK_SECRET
  on: {
    'post.published': async ({ payload, deliveryId }) => {
      await recordPublished(payload.data, deliveryId);
    },
    '*': async ({ event }) => log(event), // runs after the specific handler
  },
  revalidate: true, // drop the cache tags this event affects
  onError: (error) => report(error),
});

Events: post.published, post.failed, post.partially_failed, delivery.published, delivery.failed, delivery.delayed, account.health_changed.

Responses: 200 {"received":true}, 401 invalid_signature, 400 invalid_payload or unknown_event, 500 webhook_not_configured or handler_failed.

verifyFoPostSignature({ rawBody, signature, secret }) is exported if you want to verify by hand.

Pages Router

The Pages Router parses the body before your handler runs, which destroys the signed bytes. Re-export the config so Next hands over the raw stream:

// pages/api/fopost/webhook.ts
import { createFoPostWebhookApiRoute, foPostWebhookApiConfig } from '@fopost/next';

export const config = foPostWebhookApiConfig; // { api: { bodyParser: false } }

export default createFoPostWebhookApiRoute({
  on: { 'post.published': async ({ payload }) => recordPublished(payload.data) },
});

Caching and revalidation

foPostTags gives you stable tags to hang FoPost reads on, and the webhook drops them when the API says something changed.

import { foPostTags, revalidateFoPost } from '@fopost/next';

// Tag a read in a Server Component
await fetch(`https://api.fopost.com/v1/posts?workspace_id=${id}`, {
  headers: { 'X-API-Key': process.env.FOPOST_API_KEY! },
  next: { tags: [foPostTags.posts(id)], revalidate: 60 },
});

// Drop it by hand
await revalidateFoPost({ tags: [foPostTags.posts(id)], paths: ['/posts'] });
TagCovers
foPostTags.all()everything FoPost
foPostTags.posts(workspaceId?)a workspace's post list
foPostTags.post(postId)one post
foPostTags.accounts(workspaceId?)connected accounts
foPostTags.account(accountId)one account
foPostTags.workspaces(), foPostTags.workspace(id)workspaces
foPostTags.analytics(workspaceId?)analytics reads

revalidate: true on the webhook handler derives the tags from the event; pass an object or a function for full control.

The rest of the API

Resources, DTOs, and FoPostError are re-exported from @fopost/sdk for convenience:

import { getFoPostClient, FoPostError } from '@fopost/next';

const fopost = getFoPostClient();
await fopost.accounts.list({ workspaceId });
await fopost.ai.generateCaption({ currentCaption: 'shipping today' });

The TypeScript SDK page has the full surface, plus retries, error handling, and the transport contract.

Next

Related documentation
  • SDKs & Integrations

    Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.

  • SDKs Overview

    Every official FoPost client, what it covers, and how to pick one.

  • TypeScript SDK

    The official TypeScript and Node.js client for the FoPost API.

  • Python SDK

    The official Python client for the FoPost API.

  • PHP SDK

    The official PHP client for the FoPost API, with no framework and no HTTP library.

  • Ruby SDK

    The official Ruby client for the FoPost API, with no runtime dependencies.

  • Go SDK

    The official Go client for the FoPost API.

  • Rust SDK

    The official Rust client for the FoPost API, async and built on reqwest.

Was this helpful?

On this page