Skip to content

This website is still under development, so some information may not be accurate yet.Under development · details may not be accurate

ScriptIQ

A platform for selling and licensing browser extensions: Paddle checkout, a customer dashboard for apps, devices and invoices, and an SDK that lets extensions verify licenses offline.

ScriptIQ: All your extensions, running in order. The application catalog in a browser window.

The idea

Every paid extension needs the same plumbing: accounts, payments and tax, license checks, device limits, refunds and invoices. Rather than building that into each of my extensions, I built ScriptIQ to handle it in one place. Customers browse the catalog, buy a one-time or subscription plan, and manage their apps, devices and billing from a single dashboard. An extension links to the buyer's account through a browser approval page and receives a signed license token it can verify offline.

Implementation

  • Next.js 16 client with React 19, HeroUI v3, Tailwind CSS v4 and TanStack Query. Catalog pages render on the server and fall back to an empty state if the API is unreachable.
  • Express 5 API in TypeScript with Zod validation on every request, on PostgreSQL through Drizzle ORM (32 tables, migrations applied at startup).
  • Paddle Billing as merchant of record: webhook signatures are verified, every event is recorded so duplicates are no-ops, and stale events can't overwrite newer subscription state.
  • Cookie-based sessions with single-use refresh tokens and reuse detection, hashed 8-digit email codes, and lockout after repeated failed logins.
  • ScriptIQ Connect: a PKCE-style approval flow where only a hash of the extension's secret appears in the URL. It returns ES256-signed license tokens that a dependency-free TypeScript SDK verifies offline.
  • Admin workspace for products, plans, customers, refunds, announcements and help articles, plus an in-house error tracker that replaced Sentry.
  • Scheduled jobs retry failed Paddle events, enforce grace periods and re-sync with Paddle, each guarded by a database lock so only one instance runs it.
  • 40 Playwright API and browser tests against an isolated test database, GitHub Actions CI and multi-stage Docker images.

Challenges

  • Found and fixed a webhook check that never awaited Paddle's asynchronous signature verification, which meant forged webhooks would have been accepted.
  • Replaced a first connect flow, where the code in the URL was enough to claim the tokens, with one where the URL carries only a hash of the extension's secret.
  • Kept users signed in across scriptiq.eu and api.scriptiq.eu: auth cookies stay on the API host, and a harmless marker cookie on the site's domain drives the route guard.
  • Made subscription state robust to duplicate and out-of-order Paddle events, with tests that replay stale events against cancelled subscriptions.
  • Told two tabs refreshing at once apart from a stolen refresh token: reuse within 60 seconds is tolerated, while later reuse revokes every session for that user.

Result

ScriptIQ is in pre-launch: the domain, mailbox and transactional email are set up, but the site and API aren't public yet. The codebase is about 83,000 lines of TypeScript across the client, API and SDK, with more than 150 API route handlers, and CI runs the API test suite on every push. My Auto Convert extension already unlocks its Pro features through the Connect SDK.

Connect SDK

A single TypeScript file with no dependencies, copied into each extension.

  • connect() opens the approval page and polls for the result
  • checkLicense() verifies the ES256 license token offline
  • refreshIfNeeded() renews tokens before they expire, with a grace period
  • disconnect() revokes the session on the server
  • Storage adapters for Chrome extensions, VS Code and the browser

Security and Privacy

  • Per-endpoint rate limits and security headers
  • Anonymised IP addresses; secrets stripped from logs and error reports
  • Scheduled cleanup of old logs, webhook records and expired tokens
  • Self-service account deletion
  • The server refuses to start with an unencrypted database connection