Deck Docs

Server-side submit

Submit feedback from Node, Route Handlers, and Server Actions with @deck-io/feedback/server.

Server-side submit

@deck-io/feedback 0.2.0+ ships a dedicated server entry that works without React or a browser:

EntryImportUse for
Client (default)@deck-io/feedbackReact UI (DeckProvider, dialogs, thumbs)
Server@deck-io/feedback/serverRoute Handlers, Server Actions, Express/Fastify, workers

Use the server entry when feedback is captured outside a React tree — for example after a failed checkout API call, from a support webhook, or from a Next.js Server Action that already has the user session.

Install

Same package as the React SDK:

pnpm add @deck-io/feedback

Import from the /server subpath so bundlers never pull React into Node code paths.

Environment variable

Prefer a server-only env var (not NEXT_PUBLIC_*):

# Next.js / Node (server-only — never expose to the browser)
DECK_PUBLISHABLE_KEY=pk_...

You still use a publishable pk_… key — not a secret Public API dk_… key. The server client talks to the same widget ingest API as the browser SDK.

import { createDeckFeedbackClient } from "@deck-io/feedback/server";

const deck = createDeckFeedbackClient({
  publishableKey: process.env.DECK_PUBLISHABLE_KEY!,
  // apiUrl: "https://..." // optional override for staging
});

const result = await deck.submit({
  user: {
    id: session.userId,
    email: session.email,
    name: session.name,
  },
  submission: {
    widgetId: "wgt_your_widget_id",
    sentiment: "negative",
    message: "Checkout API timeout",
    context: { endpoint: "/api/checkout" },
  },
  meta: {
    url: request.headers.get("referer") ?? undefined,
    userAgent: request.headers.get("user-agent") ?? undefined,
  },
});

if (!result.ok) {
  // result.error, result.status
}

Why a client factory?

  • Reads the publishable key once at construction
  • Sets meta.source to "server" by default
  • Never auto-infers URL or user-agent (no window / navigator) — pass them in meta when you have them

createDeckFeedbackClient options

OptionRequiredDescription
publishableKeyYesServer-side pk_… key
apiUrlNoOverride production widget ingest URL

submit input

FieldRequiredDescription
userYes{ id, email?, name? } — stable product user id
submission.widgetIdYesWidget ID from Manage → Widgets
submission.sentimentYes"positive" | "neutral" | "negative"
submission.messageNoFree text (drives batch synthesis when present)
submission.contextNoSmall serializable product context
metaNoExplicit url, userAgent, occurredAt, source, etc.

Low-level: submitFeedback from /server

For one-off calls without a reusable client:

import { submitFeedback } from "@deck-io/feedback/server";

await submitFeedback({
  publishableKey: process.env.DECK_PUBLISHABLE_KEY!,
  urlCapture: "none", // no browser URL inference on the server
  user: { id: session.userId, email: session.email },
  submission: {
    widgetId: "wgt_...",
    sentiment: "positive",
    message: "Loved the new onboarding",
  },
  meta: {
    source: "server",
    url: request.headers.get("referer") ?? undefined,
  },
});

createDeckFeedbackClient is preferred for most server apps: it defaults urlCapture to "none" and stamps source: "server".

Next.js Route Handler example

// app/api/feedback/route.ts
import { createDeckFeedbackClient } from "@deck-io/feedback/server";
import { NextResponse } from "next/server";

const deck = createDeckFeedbackClient({
  publishableKey: process.env.DECK_PUBLISHABLE_KEY!,
});

export async function POST(request: Request) {
  const body = await request.json();
  // validate body.widgetId, body.sentiment, body.message, session user…

  const result = await deck.submit({
    user: { id: body.userId, email: body.email },
    submission: {
      widgetId: body.widgetId,
      sentiment: body.sentiment,
      message: body.message,
      context: body.context,
    },
    meta: {
      url: request.headers.get("referer") ?? undefined,
      userAgent: request.headers.get("user-agent") ?? undefined,
    },
  });

  if (!result.ok) {
    return NextResponse.json(
      { error: result.error },
      { status: result.status ?? 400 },
    );
  }

  return NextResponse.json({ submissionId: result.submissionId });
}

Client vs server

Browser (@deck-io/feedback)Server (@deck-io/feedback/server)
React requiredYes (for provider/UI)No
Env var styleNEXT_PUBLIC_DECK_PUBLISHABLE_KEYDECK_PUBLISHABLE_KEY (server-only)
URL metadataDefault path from window.locationPass explicitly via meta (or omit)
Built-in UIFeedbackDialog, ThumbsFeedbackNone — you own the UX
Same ingest APIYesYes

You can use both in one product: React widgets for in-app prompts, server client for backend-triggered feedback.

Response shape

Same as the browser SDK:

// success
{ ok: true, submissionId?: string }

// failure
{ ok: false, error: string, status?: number }

Next