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:
| Entry | Import | Use for |
|---|---|---|
| Client (default) | @deck-io/feedback | React UI (DeckProvider, dialogs, thumbs) |
| Server | @deck-io/feedback/server | Route 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/feedbackImport 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.
Recommended: createDeckFeedbackClient
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.sourceto"server"by default - Never auto-infers URL or user-agent (no
window/navigator) — pass them inmetawhen you have them
createDeckFeedbackClient options
| Option | Required | Description |
|---|---|---|
publishableKey | Yes | Server-side pk_… key |
apiUrl | No | Override production widget ingest URL |
submit input
| Field | Required | Description |
|---|---|---|
user | Yes | { id, email?, name? } — stable product user id |
submission.widgetId | Yes | Widget ID from Manage → Widgets |
submission.sentiment | Yes | "positive" | "neutral" | "negative" |
submission.message | No | Free text (drives batch synthesis when present) |
submission.context | No | Small serializable product context |
meta | No | Explicit 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 required | Yes (for provider/UI) | No |
| Env var style | NEXT_PUBLIC_DECK_PUBLISHABLE_KEY | DECK_PUBLISHABLE_KEY (server-only) |
| URL metadata | Default path from window.location | Pass explicitly via meta (or omit) |
| Built-in UI | FeedbackDialog, ThumbsFeedback | None — you own the UX |
| Same ingest API | Yes | Yes |
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
- API reference — full server exports
- Headless submit — React custom UI
- Security — keys and env placement