Libraries and templates
Start your app from a working template
One command gives you a Next.js app with the embedded page, a webhook handler that checks every signature and a manifest ready to submit. The packages under it are published on npm under the MIT licence.
sofia@pampa ~ % npx create-tanqory-app pampa-restockCreated pampa-restock/sofia@pampa ~ % cd pampa-restock && npm installsofia@pampa ~ % cp env.example .env.localsofia@pampa ~/pampa-restock % npm run devThen, at dev.tanqory.com/apps: 1. Create App → client ID, secret 2. Submit Version → appUrl, scopes 3. Test Install → a dev storesofia@pampa ~/pampa-restock % Starters
Two starters, one for each kind of build
Apps and themes each start from a template that already does the parts that are easy to get wrong.
- App templatenpx create-tanqory-app, or tanqory app init, writes a Next.js 15 and React 19 app: the page the dashboard frames, an orders webhook route, the manifest and an AGENTS.md for coding assistants.
- Theme startertanqory theme init downloads Nova, the public React theme on GitHub, and creates it in your development store.
- Tanqory CLInpm install -g @tanqory/cli. tanqory app dev frames your local app on a development store, and tanqory app deploy sends the manifest as a new version.
pampa-restock/├── src/app/embed/[storeId]/[[...slug]]/page.tsx│ the page the dashboard frames├── src/app/hooks/orders/route.ts│ orders/create, signature checked├── tanqory.app.json│ handle, URLs, scopes, webhooks├── next.config.mjs│ frame-ancestors, no X-Frame-Options├── env.example│ client ID, secret, webhook secret├── AGENTS.md│ notes for coding assistants└── package.json next, react, @tanqory/plugin-sdk@tanqory/plugin-sdk
One package for the page, the server and the manifest
The SDK has two entry points, so a serverless function never pulls in React. The page half does the handshake with the dashboard; the server half turns your app's credentials into a token for one store.
- useTanqoryAppChecks who framed the page, asks for the store, retries a slow handshake and renews its token before it expires. It returns ready, error, store and api.
- createTanqoryAppExchanges your client ID and secret for a token per store, caches it and makes one exchange when many calls need it at once. A missing scope is a 403 that is never retried.
- parseAppManifestChecks tanqory.app.json in your own build and lists every problem at once. The JSON schema gives your editor the same checks while you type.
'use client'import { useTanqoryApp } from '@tanqory/plugin-sdk/client'export default function Embed() { const { ready, error, store, api } = useTanqoryApp() if (error) return <p>{error}</p> if (!ready) return <p>Loading…</p> return ( <main> <h1>{store!.storeName}</h1> <button onClick={() => api!.get('/orders', { limit: 5 }).then(console.log) } > Load recent orders </button> </main> )}Webhooks
Every delivery is signed, and the template checks it
Tanqory signs each webhook with HMAC-SHA256 over a timestamp and the body, in the X-Tanqory-Signature header. verifyWebhookSignature checks both and turns away a delivery older than five minutes, so a captured request cannot be sent again.
- Pass the raw bodyRead the request as text before you parse it. A body parsed and written back changes its spacing and key order, and the signature no longer matches.
- The webhook secretDeliveries are signed with a secret per installation, returned as webhook_secret by the token endpoint. Your client secret is never used to sign.
- Answer 401 to a bad signatureThe review preflight sends a deliberately wrong signature and expects a refusal. The template's route already gives one.
import { verifyWebhookSignature } from '@tanqory/plugin-sdk'export async function POST(request: Request) { const raw = await request.text() if ( !verifyWebhookSignature( raw, request.headers.get('x-tanqory-signature'), process.env.TANQORY_WEBHOOK_SECRET!, ) ) { return new Response('bad signature', { status: 401 }) } const { storeId, payload } = JSON.parse(raw) console.log('new order', payload?.id, 'in', storeId) return new Response('ok')}@tanqory/app-bridge
The bridge between your page and the dashboard
Under useTanqoryApp sits App Bridge: typed messages between the dashboard and your page in its frame. Most apps never call it directly; reach for it when you build without React or need a message the hook does not cover.
- Origins on a listBoth sides check where each message came from against an explicit list and never post to any origin.
- A code in the URL, not a tokenThe frame URL carries a single-use code that expires after 30 seconds, so no token is left in browser history or server logs.
- Thirteen typed messagesRoute changes, resize, full screen, toasts, the media picker, unsaved changes, and the merchant's theme and language as they change.
import { createBridgeGuest } from '@tanqory/app-bridge'const guest = createBridgeGuest({ allowedOrigins: ['https://app.tanqory.com'],})guest.start()// storeId, permissions, locale, apiBaseUrlconst ctx = await guest.init()guest.ready()const { token } = await guest.requestToken()guest.post('ROUTE_CHANGED', { path: '/settings' })guest.on('THEME_CHANGED', ({ theme }) => setTheme(theme))Build your first app
Open a development store, run the template and see your page inside a real merchant dashboard. The app runs on your own hosting, and tanqory app deploy sends a changed manifest as a new version.
- Next.js 15 and React 19
- Signed webhooks checked
- MIT licensed packages