惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
Simon Willison's Weblog
Simon Willison's Weblog
B
Blog
V
Visual Studio Blog
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
S
SegmentFault 最新的问题
博客园 - 司徒正美
博客园 - 【当耐特】
T
Tenable Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium
S
Secure Thoughts
Hugging Face - Blog
Hugging Face - Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
IT之家
IT之家
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
大猫的无限游戏
大猫的无限游戏
PCI Perspectives
PCI Perspectives
H
Hackread – Cybersecurity News, Data Breaches, AI and More
阮一峰的网络日志
阮一峰的网络日志
P
Privacy International News Feed
N
News and Events Feed by Topic
H
Hacker News: Front Page
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
F
Full Disclosure
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Heimdal Security Blog
Project Zero
Project Zero
C
CERT Recently Published Vulnerability Notes
MyScale Blog
MyScale Blog
AI
AI
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CXSECURITY Database RSS Feed - CXSecurity.com
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
SecWiki News
SecWiki News
C
Cisco Blogs
The Last Watchdog
The Last Watchdog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building an effective Storyblok Tool Plugin with SvelteKit
Roberto B. · 2026-05-26 · via DEV Community

Storyblok Tool Plugins are useful when editors need to perform an action directly in the Visual Editor.

In this article, we will build a Tool Plugin with SvelteKit. The plugin reads the current story context, checks space-level settings, and sends the story payload to a configured webhook.

The example uses SvelteKit, but the architecture works in the same way with Next.js, Nuxt, Remix, Express, or any framework that can expose server routes.

What is a Storyblok Tool Plugin?

Storyblok plugins let developers extend Storyblok with custom features. They can improve the editing experience, integrate third-party systems, or add tools tailored to a team's workflow.

Storyblok provides different plugin types. Field Plugins extend the editing form with custom inputs. Space Plugins can add functionality at the space level. Tool Plugins add custom tool windows to the Visual Editor.

A Tool Plugin is useful when the editor needs to run an action while looking at a specific story. It appears in the Visual Editor, communicates with Storyblok via the App Bridge, and requests the current story context via postMessage.

That context is the important part. The plugin can determine which story the editor is working on and use that information to trigger an external process, call an internal API, validate content, start a translation flow, export data, or send content to another system.

For developers, this is worth knowing because it gives you a controlled way to bring project-specific workflows into the CMS. Instead of asking editors to copy IDs, open external dashboards, or remember manual steps, you can expose a small action right where the editorial decision already happens.

The technical model is also familiar. A Tool Plugin is a web application embedded in Storyblok. It has a browser UI, server routes, environment variables, OAuth when needed, and API calls. If you know how to build a small app with SvelteKit, Next.js, Nuxt, or Express, you already understand most of the building blocks.

The use case

The plugin we are going to build with this article is called Send to Flowmotion.

Storyblok webhooks are a good fit for automatic integrations. They can trigger external systems when something specific happens, such as a workflow stage change, an image upload, or a content publication. That model works well when the process should always run after an event.

Some workflows need a manual decision. An editor may want to trigger a Flowmotion workflow only for a specific story, at a specific moment, and for a specific purpose. Examples include publishing a selected article to LinkedIn, sending a message to another team, exporting content to a third-party system, or starting a custom review process.

That is the reason for this Tool Plugin. The editor opens a story in the Storyblok Visual Editor, clicks a tool in the sidebar, and sends the current story payload to an external Flowmotion webhook.

The payload is important. Because the story data is already sent to Flowmotion, the workflow often does not need extra nodes only to retrieve the content, resolve the story, or look up the selected item again.

The plugin has to handle a few things:

  • Run inside the Storyblok Visual Editor iframe.
  • Request the current story context through App Bridge.
  • Authenticate with Storyblok OAuth.
  • Read plugin settings from the Storyblok Management API.
  • Keep the webhook URL server-side.
  • Forward the story payload from a backend route.
  • Show clear loading, setup, auth, success, and error states.

This is a small feature, but it touches the main parts of a real Tool Plugin.

The architecture

The implementation has three layers:

  1. The iframe UI.
  2. The app backend.
  3. Storyblok and the external webhook.

The browser-side plugin should only handle editor interaction. It can request context from Storyblok, show states, and call your backend. It should not expose OAuth tokens, client secrets, or private webhook URLs.

The backend does the sensitive work:

  • Starts and handles Storyblok OAuth.
  • Reads the authenticated Storyblok session.
  • Calls the Management API.
  • Resolves the configured webhook URL.
  • Sends the story payload to the webhook.

The flow looks like this:

Storyblok Visual Editor
        |
        | App Bridge postMessage
        v
Tool Plugin iframe
        |
        | /api/config
        v
SvelteKit backend
        |
        | Storyblok Management API
        v
Space-level plugin settings
        |
        | /api/trigger-webhook
        v
Flowmotion webhook

Enter fullscreen mode Exit fullscreen mode

This separation matters. A Tool Plugin often runs in a browser context controlled by an editor session, while the backend has access to private credentials. Keep those boundaries clear from the beginning.

Step 1: Start with HTTPS locally

Storyblok loads Tool Plugins in an iframe. For local development, the app must be available over HTTPS.

In this project, Vite uses local certificate files when they exist:

.cert/localhost-key.pem
.cert/localhost-cert.pem

Enter fullscreen mode Exit fullscreen mode

You can create them with mkcert:

mkcert -install
mkdir -p .cert
mkcert -key-file .cert/localhost-key.pem -cert-file .cert/localhost-cert.pem localhost 127.0.0.1 ::1

Enter fullscreen mode Exit fullscreen mode

Then start the app:

bun run dev

Enter fullscreen mode Exit fullscreen mode

The local app is available at:

https://localhost:5173

Enter fullscreen mode Exit fullscreen mode

This is not only a development detail. If the iframe cannot load correctly, the rest of the plugin cannot be tested inside the Visual Editor.

Step 2: Create a small plugin shell first

Before adding App Bridge or OAuth, build the first screen.

The plugin shell should include:

  • A title.
  • A loading state.
  • An empty or waiting state.
  • A configuration status area.
  • A primary action button.
  • Secondary story metadata.
  • A compact layout suitable for the Visual Editor tools panel.

This avoids mixing UI questions with integration questions. When the UI shell is stable, you can add App Bridge and backend calls step by step.

In this project, the main UI lives in:

src/routes/+page.svelte

Enter fullscreen mode Exit fullscreen mode

The send button is only enabled when three conditions are true:

let canSend = $derived(
    Boolean(context && configStatus === 'loaded' && config?.authenticated && config.configured)
);

Enter fullscreen mode Exit fullscreen mode

The same idea applies in React, Vue, or any other framework. Compute the action state from the current context and backend configuration. Do not let the editor click an action until the plugin knows it can complete it.

For the final layout, keep the editor action high in the panel. In this example, the UI shows the story name first, then the configuration status, then the Send to Flowmotion button. Diagnostic metadata such as slug, story ID, space ID, and language appears below the action.

Step 3: Request Storyblok context with App Bridge

The Tool Plugin receives story data from Storyblok through postMessage.

The plugin sends a message to the parent frame:

function postToStoryblok(event: string, data: Record<string, unknown> = {}) {
    const message = {
        action: 'tool-changed',
        tool: PLUGIN_SLUG,
        event,
        ...data
    };

    window.parent.postMessage(message, '*');
}

Enter fullscreen mode Exit fullscreen mode

When the component mounts, it resizes the iframe and requests context:

onMount(() => {
    window.addEventListener('message', handleMessage);
    postToStoryblok('heightChange', { height: IFRAME_HEIGHT });
    postToStoryblok('getContext');

    return () => {
        window.removeEventListener('message', handleMessage);
    };
});

Enter fullscreen mode Exit fullscreen mode

The plugin then normalizes incoming messages. This is useful because payloads can be shaped differently depending on the source or wrapper:

function handleMessage(event: MessageEvent) {
    const message = normalizeContextMessage(event.data);
    if (!message) return;

    if (
        (message.action === 'get-context' || message.action === 'loaded' || message.story) &&
        message.story
    ) {
        setContext({
            story: message.story,
            spaceId: message.spaceId,
            language: message.language
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

After the story context is available, the UI can show the story name and ask the backend whether the plugin is configured.

Step 4: Add a local mock mode

App Bridge only works when the plugin runs inside Storyblok. That can slow down local iteration.

A mock mode helps you test the UI outside the Visual Editor:

const ENABLE_MOCK_CONTEXT = env.PUBLIC_ENABLE_MOCK_CONTEXT === 'true';

const mockContext = {
    story: {
        id: 123456789,
        uuid: 'mock-story-uuid',
        name: 'Article created from local mock context',
        full_slug: 'articles/article-created-from-local-mock-context',
        content: {}
    },
    spaceId: '123456',
    language: 'default'
};

Enter fullscreen mode Exit fullscreen mode

This is useful for screenshots, local UI work, and debugging. Keep it disabled in production:

PUBLIC_ENABLE_MOCK_CONTEXT=false

Enter fullscreen mode Exit fullscreen mode

The principle is framework-independent. Give yourself a local fallback, but make sure it cannot mask real production behavior.

Step 5: Define the backend contract early

Before calling the Storyblok Management API, create the backend routes and return mock responses.

This project uses two routes:

GET  /api/config
POST /api/trigger-webhook

Enter fullscreen mode Exit fullscreen mode

GET /api/config answers this question:

Can the current user and space send this story to Flowmotion?

Enter fullscreen mode Exit fullscreen mode

A configured response can look like this:

{
    "authenticated": true,
    "configured": true,
    "settings": {
        "hasWebhookUrl": true,
        "httpMethod": "POST"
    }
}

Enter fullscreen mode Exit fullscreen mode

An auth-required response can look like this:

{
    "authenticated": false,
    "configured": false,
    "connectUrl": "https://localhost:5173/api/connect/storyblok",
    "missing": ["Connect Storyblok before reading space-level settings."]
}

Enter fullscreen mode Exit fullscreen mode

This contract lets the frontend handle states before the real integration exists. It also makes the implementation easier to port to other frameworks. In Next.js this would be an API route. In Nuxt it would be a server route. In Express it would be a normal endpoint.

Step 6: Add OAuth with the official helper

The plugin needs OAuth because the backend reads space-level plugin settings through the Storyblok Management API.

The project uses Storyblok's official helper:

bun add @storyblok/app-extension-auth

Enter fullscreen mode Exit fullscreen mode

The required environment variables are:

APP_CLIENT_ID=your-storyblok-app-client-id
APP_CLIENT_SECRET=your-storyblok-app-client-secret
APP_URL=https://localhost:5173

Enter fullscreen mode Exit fullscreen mode

The auth helper is wrapped in a small server module:

export function getAuthParams(): AuthHandlerParams {
    const clientId = env.APP_CLIENT_ID;
    const clientSecret = env.APP_CLIENT_SECRET;
    const baseUrl = env.APP_URL;

    if (!clientId || !clientSecret || !baseUrl) {
        throw new Error('Missing APP_CLIENT_ID, APP_CLIENT_SECRET, or APP_URL.');
    }

    return {
        clientId,
        clientSecret,
        baseUrl,
        endpointPrefix: '/api/connect',
        sessionKey: 'send-to-flowmotion.sb.auth',
        successCallback: '/',
        errorCallback: '/401'
    };
}

Enter fullscreen mode Exit fullscreen mode

The SvelteKit route delegates OAuth handling to the helper:

export const GET: RequestHandler = handleAuth;
export const POST: RequestHandler = handleAuth;

Enter fullscreen mode Exit fullscreen mode

The editor starts OAuth through:

/api/connect/storyblok

Enter fullscreen mode Exit fullscreen mode

The callback URL is:

/api/connect/callback

Enter fullscreen mode Exit fullscreen mode

For production, register the full HTTPS callback URL in Storyblok:

https://your-plugin.example.com/api/connect/callback

Enter fullscreen mode Exit fullscreen mode

Step 7: Read space-level settings

The webhook URL should not be hardcoded in the frontend. It should come from Storyblok space-level settings and be read on the server.

In Storyblok, configure the installed Tool Plugin with:

webhook_url=https://your-flowmotion-webhook.example.com/...
http_method=POST

Enter fullscreen mode Exit fullscreen mode

The backend uses the OAuth session to call the Management API:

const response = await fetch(
    `${getManagementBaseUrl(session.region)}/v1/spaces/${session.spaceId}/app_provisions/`,
    {
        headers: {
            Authorization: `Bearer ${session.accessToken}`,
            Accept: 'application/json'
        }
    }
);

Enter fullscreen mode Exit fullscreen mode

The plugin then finds the installed app provision by slug and reads space_level_settings:

const settings = readSpaceLevelSettings(provision.space_level_settings);
const webhookUrl = readWebhookUrlSetting(settings.webhook_url);
const httpMethod = readStringSetting(settings.http_method)?.toUpperCase();

Enter fullscreen mode Exit fullscreen mode

The code validates both settings:

function getMissingSettings(config: Pick<FlowmotionConfig, 'httpMethod' | 'webhookUrl'>) {
    const missing: string[] = [];

    if (!config.webhookUrl) {
        missing.push('Add a valid absolute webhook_url setting.');
    }

    if (!config.httpMethod) {
        missing.push('Add the http_method setting.');
    } else if (!ALLOWED_HTTP_METHODS.has(config.httpMethod)) {
        missing.push('Set http_method to POST.');
    }

    return missing;
}

Enter fullscreen mode Exit fullscreen mode

This is a good pattern for Tool Plugins. Let Storyblok store space-specific configuration, then resolve it server-side when the editor performs an action.

Step 8: Forward the story payload from the backend

The frontend calls the backend with the current story context:

const response = await fetch('/api/trigger-webhook', {
    method: 'POST',
    headers: {
        'content-type': 'application/json',
        'X-Storyblok-Space-Id': String(context.spaceId ?? ''),
        'X-Storyblok-User-Id': getUserId() ?? ''
    },
    body: JSON.stringify({
        story: context.story,
        spaceId: context.spaceId,
        language: context.language ?? 'default'
    })
});

Enter fullscreen mode Exit fullscreen mode

The server validates the request:

function validateTriggerPayload(payload: TriggerWebhookRequest) {
    if (!isRecord(payload.story)) {
        return 'A story payload is required.';
    }

    if (typeof payload.spaceId !== 'string' && typeof payload.spaceId !== 'number') {
        return 'A Storyblok space ID is required.';
    }

    if (payload.language !== undefined && typeof payload.language !== 'string') {
        return 'Language must be a string.';
    }
}

Enter fullscreen mode Exit fullscreen mode

Then it loads the Storyblok session, resolves the Flowmotion config, and forwards the payload:

const forwardedPayload = {
    plugin: getPluginSlug(),
    story: payload.story,
    spaceId: payload.spaceId,
    language: payload.language ?? 'default',
    triggeredAt: new Date().toISOString()
};

const response = await fetch(config.webhookUrl, {
    method: config.httpMethod,
    headers: {
        'content-type': 'application/json'
    },
    body: JSON.stringify(forwardedPayload)
});

Enter fullscreen mode Exit fullscreen mode

This keeps the webhook URL out of the browser. It also gives you one place to handle retries, logging, payload validation, rate limits, or future transformations.

Step 9: Return useful error messages

When the webhook call fails, a generic 502 is not enough.

The backend should return a useful but safe message:

if (!response.ok) {
    const responseBody = await readResponseBodySnippet(response);
    const upstreamMessage = [
        `Flowmotion webhook returned status ${response.status}`,
        response.statusText && `(${response.statusText})`,
        responseBody && `: ${responseBody}`
    ]
        .filter(Boolean)
        .join(' ');

    return json(
        {
            sent: false,
            error: upstreamMessage,
            upstreamStatus: response.status
        },
        { status: 502 }
    );
}

Enter fullscreen mode Exit fullscreen mode

The frontend reads that JSON and shows the actual error:

const result = (await response.json().catch(() => ({}))) as TriggerWebhookResponse;

if (!response.ok) {
    throw new Error(result.error ?? `Webhook request failed with status ${response.status}.`);
}

Enter fullscreen mode Exit fullscreen mode

This helps during development and support. The editor can report a specific message, and the developer can tell whether the failure is a bad URL, an auth issue, a validation error, or an upstream server problem.

Step 10: Document production setup

The README should include more than bun run dev.

For this plugin, the production documentation includes:

  • Required environment variables.
  • Local HTTPS setup.
  • Storyblok Tool Plugin registration.
  • OAuth callback URL.
  • Space-level settings.
  • Deployment notes for Vercel or Netlify.
  • Production caveats.
  • A verification checklist.

The most important production environment variables are:

PUBLIC_STORYBLOK_TOOL_PLUGIN_SLUG=your-org@send-to-flowmotion
PUBLIC_DEBUG_APP_BRIDGE=false
PUBLIC_ENABLE_MOCK_CONTEXT=false
APP_CLIENT_ID=your-storyblok-app-client-id
APP_CLIENT_SECRET=your-storyblok-app-client-secret
APP_URL=https://your-plugin.example.com

Enter fullscreen mode Exit fullscreen mode

Set APP_URL to the deployed HTTPS URL. It must match the URL used in Storyblok OAuth settings.

What changes in Next.js, Nuxt, or another framework?

The framework changes the file structure, not the architecture.

In SvelteKit, we used:

src/routes/+page.svelte
src/routes/api/config/+server.ts
src/routes/api/trigger-webhook/+server.ts
src/routes/api/connect/[...slugs]/+server.ts
src/lib/server/storyblok-auth.ts
src/lib/server/storyblok-management.ts

Enter fullscreen mode Exit fullscreen mode

In Next.js, the same responsibilities map to:

app/page.tsx
app/api/config/route.ts
app/api/trigger-webhook/route.ts
app/api/connect/[...slugs]/route.ts
lib/server/storyblok-auth.ts
lib/server/storyblok-management.ts

Enter fullscreen mode Exit fullscreen mode

In Nuxt, they map to:

app.vue or pages/index.vue
server/api/config.get.ts
server/api/trigger-webhook.post.ts
server/api/connect/[...slugs].ts
server/utils/storyblok-auth.ts
server/utils/storyblok-management.ts

Enter fullscreen mode Exit fullscreen mode

The key decisions stay the same:

  • Use App Bridge in the browser.
  • Use OAuth on the backend.
  • Read Management API data server-side.
  • Keep private config out of the browser.
  • Return explicit state to the UI.

Production hardening

The example is functional, but a production plugin should go further.

Recommended next steps:

  • Store OAuth sessions in durable storage when deploying to serverless platforms.
  • Validate App Bridge tokens server-side before accepting iframe requests.
  • Add stricter request validation with a schema library.
  • Add rate limiting for mutation or webhook routes.
  • Add structured logging without exposing secrets.
  • Avoid storing secret webhook tokens directly in Storyblok space-level settings when possible.

Storyblok space-level settings are useful for configuration, but they are not a secret manager. If the webhook URL contains sensitive tokens, consider storing a profile key in Storyblok and resolving the actual URL on your backend.

Business value

A Tool Plugin is valuable when it removes context switching for editors.

In this example, the editor does not need to copy a story ID, open another tool, or manually trigger an external workflow. The action is available next to the story they are already editing.

For developers and solution engineers, the value is also architectural:

  • The plugin uses the editor context instead of duplicating UI.
  • Configuration is managed per Storyblok space.
  • Sensitive work stays on the backend.
  • The same pattern can trigger webhooks, workflows, previews, translations, QA checks, or external publishing processes.

This is the main reason to build a Tool Plugin. It connects editorial work with operational workflows while keeping the integration controlled and maintainable.

Conclusion

Building an effective Storyblok Tool Plugin is less about the frontend framework and more about the integration boundaries.

Use App Bridge to get editor context. Use OAuth and the Management API on the backend. Store space-specific configuration in Storyblok. Forward sensitive requests from server routes. Give editors clear states and useful error messages.

SvelteKit works well for this because pages and server routes live in the same project. The same approach works with Next.js, Nuxt, or any framework that gives you a browser UI and backend endpoints.

The important part is the shape of the system: small steps, clear contracts, server-side secrets, and a plugin experience that fits naturally into the Storyblok Visual Editor.