Skip to content
Log inStart free

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.

pampa: zsh
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/
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.
src/app/embed/[storeId]/[[...slug]]/page.tsx
'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.
src/app/hooks/orders/route.ts
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.
bridge.ts
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