Plugins

SaaS kit

The official saas plugin (paid) turns a InsightSite site into a SaaS product: sign-up and sign-in, a user dashboard and user management in the admin. It covers what SaaS boilerplates such as ShipFast or FlareStarter offer, on top of the CMS, locales, SEO and shop you already have, so no second project is needed. The plugin ships with the framework (plugins/saas).

node node_modules/nuxtcms/scripts/plugin.mjs add saas
pnpm build:cf && pnpm deploy

Included: user accounts, plans and billing, plan-gated features, credits, API keys and teams.

Features

FeatureDetails
Sign-up and sign-inE-mail + password; passwordless e-mail link (one-time, valid 15 minutes, signs up unknown addresses); forgotten password; optional "verify the e-mail before signing in"
Social sign-inGoogle, GitHub, WeChat Open Platform website apps (QR code); links to an existing account by e-mail only when the provider says the address is verified
User dashboard /accountProfile, change / set the password, sign out everywhere, plan and credits; account:main and account:aside slots for the site or other plugins
Admin User accountsSearch, status filter, sign-in methods, mark verified, disable / enable, sign out everywhere, view as the user (logged; the dashboard shows a banner)
SecurityOwn cookie and signing key (a user token can never pass as an admin token); a password change, disabling or "sign out everywhere" ends every older session; one-time links are stored hashed and work once; per-IP and per-account rate limits plus the site captcha on sign-in, sign-up and e-mails; redirects stay on the site

Settings

Under Admin → Plugins → SaaS kit → Configure: open sign-ups, password sign-in, e-mail link sign-in, required e-mail verification, the page after sign-in, and the social sign-in keys. E-mail links, verification and password resets need SMTP under Admin → E-mail settings (in local development without SMTP the link is printed in the dev server console).

Keys may come from the environment instead (leave the fields empty): NUXT_SAAS_GOOGLE_CLIENT_ID, NUXT_SAAS_GOOGLE_CLIENT_SECRET, NUXT_SAAS_GITHUB_CLIENT_ID, NUXT_SAAS_GITHUB_CLIENT_SECRET, NUXT_SAAS_WECHAT_APP_ID, NUXT_SAAS_WECHAT_APP_SECRET.

Callback URLs to register with the providers:

ProviderCallback
Google (OAuth client ID, web application)https://your-domain/api/saas/oauth/google/callback
GitHub (OAuth App)https://your-domain/api/saas/oauth/github/callback
WeChat Open Platform (website app)authorized callback domain your-domain

Using it in a site

Protect a page with the saas-auth middleware and read the user with useSaasUser() (user data loads in the browser only, so pages stay cacheable by the CDN):

<script setup lang="ts">
definePageMeta({ middleware: 'saas-auth' })
const { user } = useSaasUser()
</script>

In server routes:

export default defineEventHandler(async (event) => {
  const user = await requireSaasUser(event) // 401 when signed out
  return { plan: user.plan, credits: user.credits }
})

getSaasUser(event) returns null for visitors. Extend the dashboard:

// app.config.ts
export default defineAppConfig({
  nuxtcms: {
    slots: {
      'account:main': { MyUsageCard: 10 },
      'header:actions': { SaasAccountButton: false }, // hide the header sign-in button
    },
  },
})

With the shop enabled too, the header shows both the shop member and the SaaS user entry; hide one as above.

The tables {prefix}saas_users, saas_identities and saas_tokens are created on first use (SQLite / D1, PostgreSQL, MySQL).

Plans and billing

Create plans under Admin → Plans: a slug (e.g. pro, stored as the user's plan), a level (higher = better), monthly / yearly prices, credits per month, name / description / features per locale, "most popular" and "for sale". free is built in and needs no plan.

ProviderHow it works
Stripe subscriptionsCreate monthly / yearly recurring prices in Stripe and enter their IDs (price_...) on the plan. Users subscribe through Stripe Checkout and Stripe renews; on the billing page they open the Stripe customer portal to change the card, get invoices, switch or cancel. Webhook: /api/saas/webhooks/stripe, events customer.subscription.*, checkout.session.completed, invoice.paid
WeChat Pay / AlipayBuy a month or a year (currency CNY): WeChat QR code on desktop (Native), H5 in mobile browsers (enable it on the merchant platform), Alipay desktop / mobile website pay. Renewing the same plan extends it from its current end; another plan starts on the day of payment. Callbacks: /api/saas/webhooks/wechat, /api/saas/webhooks/alipay

Payment keys go under Admin → Plugins → SaaS kit → Configure or in the environment (NUXT_SAAS_STRIPE_SECRET_KEY, NUXT_SAAS_WECHAT_PAY_MCH_ID, ...). Empty fields fall back to the shop plugin's payment settings, so a site does not enter them twice (except the Stripe webhook signing secret: each endpoint has its own). On demo sites (NUXT_DEMO_MODE=true) WeChat Pay and Alipay are simulated.

Pages:

PageWhat it does
/pricingPricing table (monthly / yearly switch, most popular, yearly saving). Put <SaasPricing /> on any page, or replace the page with the site's own pages/pricing.vue
/account/billingPlan and its end / renewal date, credits, payment history, "manage subscription & invoices" (Stripe portal), renew, upgrade
/account/pay/<order>WeChat QR code or Alipay redirect, polls the payment result
Admin Plans, RevenuePlan management; revenue of the last 30 days, paying users, active subscriptions, orders; under User accounts set a plan, end date and credits by hand (gifts, bank transfers, refunds)

Every payment emits the CMS event saas.payment.paid for the robot / webhook plugins.

Plan-gated features

Pages (guidance in the browser: visitors go to sign-in, lower plans to /pricing):

<script setup lang="ts">
definePageMeta({ saasPlan: 'pro' }) // Pro or a higher level; ['team', 'enterprise'] = only those
</script>

APIs (the real protection is on the server):

export default defineEventHandler(async (event) => {
  const user = await requireSaasPlan(event, 'pro') // 401 signed out, 403 plan too low
  return { ok: true, credits: user.credits }
})

To show or hide paid UI: GET /api/saas/billing/access?plan=pro answers { allowed, plan }, or use useSaasUser().user.value.plan with saasPlanAllows().

Credits

Every user has a credit balance and a ledger (reason, related order or request, balance after each change). Plan payments add the plan's credits per month; admins add or remove credits by hand. Spend credits for metered features on the server:

export default defineEventHandler(async (event) => {
  const user = await requireSaasApiUser(event)
  const { balance } = await spendSaasCredits(event, user.id, 5, 'ai.summary', { ref: 'post-42' })
  // not enough credits: 402 saas.credits.insufficient ({ needed, balance }), nothing is spent
  return { ok: true, balance }
})

Spending is one conditional UPDATE, so concurrent requests never go below zero; addSaasCredits(event, userId, n, reason) grants or refunds. Users see the ledger on the billing page.

API keys

Users create keys at /account/api-keys (nck_..., shown once, only a hash is stored), name them, set an expiry, see the last use and revoke them; up to 10 each. Requests send Authorization: Bearer nck_... or X-API-Key.

const user = await requireSaasApiUser(event)        // browser session or API key
const user = await requireSaasApiPlan(event, 'pro') // and on Pro or higher

Example endpoint: GET /api/saas/v1/me. The plugin settings switch keys off, set the lowest plan for them (e.g. pro) and the requests per minute per key (default 120). Keys cannot be created while an admin views as the user, and a key never opens the admin.

Teams

Plans have team seats (owner included; 1 = no team). A user whose plan has more than one seat creates a team at /account/team and invites members by e-mail (valid 7 days; when the e-mail cannot be sent the page shows the invitation link). The invitee signs in or signs up with that address and accepts.

  • Members share the owner's plan: requireSaasPlan, the saasPlan page meta and /api/saas/billing/access use the highest level of the user's own plan and their teams'. Credits and API keys stay personal.
  • Roles: owner (deletes the team, makes admins), admin (invites, removes members), member (may leave).
  • When the owner's plan ends or drops, members lose it too; with fewer seats the earliest members keep it.
  • Teams can be switched off in the plugin settings.

Lifecycle e-mails

With "Automatic e-mails" on in the plugin settings (off by default) a scheduled task checks hourly and sends:

  • Welcome once, within 3 days of sign-up;
  • Plan ending reminders some days before a paid term ends ("Reminder days", default 7,1); not for Stripe subscriptions that renew by themselves;
  • Plan ended once, inviting the user back.

Every e-mail is claimed in saas_mail_log before it is sent, so parallel runs never send twice; a failed send is retried on the next run. It needs e-mail (SMTP) and the cron worker (NUXT_CRON_SECRET); the endpoint is POST /api/cron/saas/lifecycle.

Testing

pnpm smoke:saas --base http://localhost:3192 --db .data/saas-demo.sqlite runs the whole flow against a dev server (launch config nuxtcms-dev-saas): sign-up, sign-in, password change, e-mail links, reset, verification, settings switches, admin actions and forged tokens, plus plans, WeChat Pay / Alipay callbacks (forged ones too), renewals and upgrades, Stripe webhooks, admin plan changes, the credit ledger, API keys (rate limit, plan rule, revoking) and teams (invitations, seats, roles, shared plan). It changes that database: never point it at a real site.