Subscriptions reference
This page lists the hooks, routes, table and translation keys of Subscriptions. It is for app developers, in server mode. Set up the module first: Overview.
Hooks
Your app answers these Nitro hooks from a server plugin (server/plugins/*.ts).
| Hook | When | Payload |
|---|---|---|
subscriptions:checkout:before | Before a new checkout or a plan change, after validation. Throw to deny. Not run for resume. | CheckoutBeforeContext |
subscriptions:plans:filter | Whenever plans are listed or checked, so it also gates checkout. Change ctx.plans. | PlansFilterContext |
subscriptions:invoice | A subscription invoice was paid, failed or voided. Fired only with Payments installed. | { invoice: InvoicePaymentEvent } |
type CheckoutBeforeContext = {
event: H3Event
userId: number
planKey: string | null
priceId: string
kind: 'checkout' | 'upgrade' | 'downgrade'
params: Stripe.Checkout.SessionCreateParams // only used when kind === 'checkout'
}
type PlansFilterContext = {
event: H3Event | undefined // undefined outside a request
plans: ResolvedPlan[] // a copy of the cached plans
}
type ResolvedPlan = { key: string, label: string, description?: string, rank: number, features?: string[], prices: PlanPrice[] }
type PlanPrice = {
id: string
amount: number | null // minor units
currency: string
interval: 'day' | 'week' | 'month' | 'year'
intervalCount: number
trialPeriodDays: number | null
lookupKey: string | null
}
The default params hold mode: 'subscription', the line item, success_url, cancel_url,
client_reference_id (the user id) and subscription_data.metadata (userId, planKey). The Stripe customer is
added after the hook.
This plugin closes sign-up out of season:
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('subscriptions:checkout:before', (ctx) => {
if (!isSignupSeason(new Date())) throw createError({ statusCode: 403, statusMessage: 'Sign-up is closed' })
if (ctx.kind === 'checkout') ctx.params.metadata = { ...ctx.params.metadata, source: 'website' }
})
})
Routes
Routes the module registers, and who may call them.
| Method | Path | Auth | Status | Purpose |
|---|---|---|---|---|
GET | /api/subscriptions/plans | public | 200 | { plans: ResolvedPlan[] }, after the plans:filter hook. |
GET | /api/subscriptions/me | subscriptions:read | 200 | MeResponse: all subscriptions and activePlan. |
POST | /api/subscriptions/me/checkout | session only | 200 | A new checkout or a plan change. See Checkout. |
POST | /api/subscriptions/me/portal | session only | 200 | { url } of a billing portal session. Body { returnUrl? }, same origin. |
GET | /api/admin/users/:id/membership | users:read | 200 | The current membership and the plans staff can grant. |
GET | /api/admin/users/:id/membership/history | users:read | 200 | The full history, ended rows included, as MeResponse. |
POST | /api/admin/users/:id/membership | subscriptions:manage | 201 | Grants a free membership. Body { planKey, endsAt? }. Answers a SubscriptionDto. |
DELETE | /api/admin/users/:id/membership | subscriptions:manage | 204 | Revokes a free membership. 404 not_found when there is none. |
POST | /api/admin/users/:id/membership/payment-link | subscriptions:manage | 200 | Creates a checkout link, and emails it on request: { url, emailed }. |
- Checkout and the portal are billing, so they need a session. An API token or access
token gets
403 session_required, whatever its scopes. - A token with the
subscriptions:readpermission can readme.subscriptions:manageis session only, so a token gets403 session_requiredon the staff change routes. - A
returnUrlon another origin answers400 bad_request. - Granting answers
400for an unknown plan or a pastendsAt, and409when the user has a paid subscription. - Path builders are
subscriptionsApifrom@frigear-nu/subscriptions/api.
type SubscriptionDto = {
id: string
planKey: string | null
priceId: string
status: string // raw Stripe status
cancelAtPeriodEnd: boolean
currentPeriodStart: string | null // ISO
currentPeriodEnd: string | null
endedAt: string | null
source?: 'stripe' | 'manual' // 'manual' for free memberships granted by staff
}
type MeResponse = {
subscriptions: SubscriptionDto[]
activePlan: string | null // plan of the entitled subscription with the latest period end
}
type AdminMembershipResponse = { // GET /api/admin/users/:id/membership
plans: Array<{ key: string, label: string, rank: number }>
current: {
id: string
planKey: string | null
status: string
source: 'stripe' | 'manual'
currentPeriodEnd: string | null
cancelAtPeriodEnd: boolean
} | null
}
// POST .../membership/payment-link takes { planKey?, priceId?, email? }
type AdminPaymentLinkResponse = { url: string, emailed: boolean }
Database
Tables the module adds. Generate and commit a migration after installing.
The module adds one subscriptions table. The same migration must also cover the users.stripe_customer_id column of
Stripe.
| Column | Type | Notes |
|---|---|---|
id | text, primary key | The Stripe subscription id (sub_...), or manual_<uuid> for a free membership. |
userId | integer | Indexed, no foreign key. The webhook finds it through users.stripeCustomerId. |
planKey | text, nullable | null when the price is in no configured plan, so config changes lose no data. |
priceId | text | Empty for a free membership. |
status | text | The raw Stripe status. |
cancelAtPeriodEnd | boolean | |
currentPeriodStart, currentPeriodEnd, endedAt | timestamps, nullable | On a free membership, currentPeriodEnd is its optional end date. |
source | text | stripe (default) or manual. |
rawItems | JSON, nullable | Item id, price id, lookup key and quantity. |
createdAt, updatedAt | timestamps |
Rows are never deleted. An ended subscription gets endedAt.
Translations
Override any string in your app's i18n/locales/<code>.json.
The module registers da and en files with Module Kit. Components read them
through useModuleT('subscriptions.<group>'), and parameters use {name}.
| Group | Keys |
|---|---|
plans.* | loading, empty, current, subscribe, upgrade, downgrade, trial ({days}), confirmTitle, confirmBody ({plan}), confirm, cancel, select, resubscribe and the confirmation texts. |
interval.* | day, week, month, year, their plurals, and quarter. |
status.* | loading, title, none, renews ({date}), cancels ({date}), manage, resubscribe, the status names, emptyTitle, emptyDescription and the card texts. |
success.*, cancel.* | The page texts: title, body and back ("Back to membership"). |
errors.* | checkout, portal. |
menu.*, admin.*, overview.* | The account menu item, the admin tab, card and panel, and the account overview summary. |
catalog.<planKey>.* | label, description and features (an array) per plan. They fall back to the plan config. |
Next steps
- Subscriptions server API – the checkout rules these hooks plug into.
- Module Kit translations – how locale files merge with the module's.