{"id":50834966,"url":"https://github.com/aashahin/subscriptions-sdk","last_synced_at":"2026-06-14T02:32:40.654Z","repository":{"id":346232505,"uuid":"1185960436","full_name":"aashahin/subscriptions-sdk","owner":"aashahin","description":"Type-safe subscription plans, feature gates, usage limits, invoices, and Elysia integration for multi-tenant TypeScript applications.","archived":false,"fork":false,"pushed_at":"2026-05-06T06:40:45.000Z","size":92,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-06T08:40:12.773Z","etag":null,"topics":["billing","elysia","elysiajs","feature-flags","multi-tenant","multitenancy","prisma","saas","subscriptions","usage-limits"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@abshahin/subscriptions","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aashahin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-19T05:42:34.000Z","updated_at":"2026-05-06T06:40:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aashahin/subscriptions-sdk","commit_stats":null,"previous_names":["aashahin/subscriptions-sdk"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aashahin/subscriptions-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fsubscriptions-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fsubscriptions-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fsubscriptions-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fsubscriptions-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aashahin","download_url":"https://codeload.github.com/aashahin/subscriptions-sdk/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fsubscriptions-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34307683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["billing","elysia","elysiajs","feature-flags","multi-tenant","multitenancy","prisma","saas","subscriptions","usage-limits"],"created_at":"2026-06-14T02:32:40.057Z","updated_at":"2026-06-14T02:32:40.649Z","avatar_url":"https://github.com/aashahin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @abshahin/subscriptions\n\nType-safe subscription plans, feature gates, usage limits, invoices, and Elysia integration for TypeScript applications.\n\nThe package ships with:\n\n- a Prisma database adapter\n- an optional cache adapter interface\n- an optional Moyasar payment adapter\n- an optional Elysia integration with routes and controller macros\n\nThe service layer is runtime-neutral and uses web-standard primitives for binary payloads and crypto-friendly flows. The current production integration uses tenant-scoped subscriptions, but the core package still models the subscribed entity as a generic subscriber.\n\n## What It Solves\n\n- Define typed subscription features once\n- Store plan overrides as JSON while keeping feature access type-safe\n- Enforce boolean feature access and numeric usage limits\n- Manage subscription lifecycle: create, change plan, cancel, pause, resume, reactivate, renew\n- Verify payment webhooks without coupling the core services to one provider\n- Generate invoice HTML and, in Node.js environments, invoice PDFs\n\n## Runtime Support\n\n- Core services and payment interfaces are runtime-neutral and accept webhook payloads as `string | Uint8Array`\n- Existing Node.js callers can still pass `Buffer`, because `Buffer` extends `Uint8Array`\n- Invoice PDF generation is Node.js-only because it depends on filesystem template loading and `puppeteer-html-pdf`\n\n## Installation\n\n```bash\nbun add @abshahin/subscriptions\n```\n\nOptional peer dependencies used by common integrations:\n\n```bash\nbun add elysia @prisma/client\n```\n\nOptional peer dependency for Node.js invoice PDF generation:\n\n```bash\nbun add puppeteer-html-pdf\n```\n\nIf you only use the service layer, Prisma adapter, or webhook handling, you do not need the PDF dependency.\n\n## Quick Start\n\n### 1. Define Features\n\n```ts\nimport { defineFeatures } from \"@abshahin/subscriptions\";\n\nexport const features = defineFeatures({\n  analyticsEnabled: {\n    type: \"boolean\",\n    default: true,\n    description: \"Visitor analytics and reporting\",\n  },\n  customDomain: {\n    type: \"boolean\",\n    default: true,\n    description: \"Connect a custom domain\",\n  },\n  maxCourses: {\n    type: \"limit\",\n    default: -1,\n    description: \"Maximum number of courses\",\n  },\n  maxProducts: {\n    type: \"limit\",\n    default: -1,\n    description: \"Maximum number of products\",\n  },\n  transactionFee: {\n    type: \"rate\",\n    default: 5,\n    description: \"Platform transaction fee percentage\",\n  },\n});\n\nexport type AppFeatures = typeof features;\n```\n\n### 2. Create a Subscriptions Instance\n\n```ts\nimport { createSubscriptions } from \"@abshahin/subscriptions\";\nimport type { CacheAdapter } from \"@abshahin/subscriptions/adapters/cache\";\nimport { prismaAdapter } from \"@abshahin/subscriptions/adapters/prisma\";\nimport { db } from \"./db\";\nimport { features } from \"./features\";\n\nconst cacheAdapter: CacheAdapter = {\n  async get(key) {\n    return redis.get(key);\n  },\n  async set(key, value, ttlSeconds) {\n    await redis.set(key, value, { ttl: ttlSeconds });\n  },\n  async delete(key) {\n    await redis.del(key);\n  },\n  async deletePattern(pattern) {\n    await redis.deleteByPattern(pattern);\n  },\n};\n\nexport const subscriptions = createSubscriptions({\n  database: prismaAdapter(db),\n  features,\n  cache: cacheAdapter,\n  options: {\n    subscriberType: \"tenant\",\n    trialDays: 14,\n    gracePeriodDays: 3,\n    defaultCurrency: \"USD\",\n    cacheTtlSeconds: 300,\n  },\n});\n```\n\n### 3. Use the Service Layer\n\n```ts\nconst tenantId = \"tenant_123\";\n\nif (await subscriptions.can(tenantId, \"analyticsEnabled\")) {\n  console.log(\"analytics enabled\");\n}\n\nconst usage = await subscriptions.remaining(tenantId, \"maxProducts\");\nconsole.log(usage.remaining);\n\nawait subscriptions.use(tenantId, \"maxProducts\");\nawait subscriptions.release(tenantId, \"maxProducts\");\n\nconst fee = await subscriptions.permissions.getRate(\n  tenantId,\n  \"transactionFee\",\n);\n```\n\n## Core Model\n\n### Feature Types\n\n`defineFeatures` supports three feature kinds:\n\n- `boolean`: enable or disable a capability\n- `limit`: numeric usage caps, with `-1` meaning unlimited\n- `rate`: numeric values such as fees or delays\n\nPlan records only store overrides. Any omitted feature falls back to the default declared in `defineFeatures`.\n\n### Subscriber Model\n\nThe package refers to the subscribed entity as a subscriber. That can be either:\n\n- a tenant, when a whole workspace or organization shares a subscription\n- a user, when each user owns their own subscription\n\n`options.subscriberType` sets the default type for newly created subscriptions. The current Prisma adapter persists subscriber IDs through the `tenantId` column, so tenant-based usage is the most mature path and the one used in the backend project.\n\n## Service API\n\n### Plans\n\n```ts\nconst plan = await subscriptions.plans.create({\n  name: \"Pro\",\n  description: \"For growing teams\",\n  price: 49,\n  currency: \"USD\",\n  interval: \"monthly\",\n  trialDays: 14,\n  features: {\n    customDomain: true,\n    maxProducts: 1000,\n    transactionFee: 2.5,\n  },\n});\n\nconst plans = await subscriptions.plans.list({ activeOnly: true });\nconst current = await subscriptions.plans.get(plan.id);\nconst duplicated = await subscriptions.plans.duplicate(plan.id, {\n  name: \"Pro Annual\",\n  interval: \"yearly\",\n});\n```\n\n### Subscriptions\n\n```ts\nconst subscription = await subscriptions.subscriptions.create(\n  tenantId,\n  plan.id,\n  {\n    trialDays: 14,\n    gatewayCustomerId: \"token_or_customer_id\",\n  },\n);\n\nawait subscriptions.subscriptions.changePlan(tenantId, \"plan_enterprise\", {\n  prorate: true,\n  verifiedTokenId: \"verified_token_id\",\n});\n\nawait subscriptions.subscriptions.cancel(tenantId, { immediately: false });\nawait subscriptions.subscriptions.resume(tenantId);\nawait subscriptions.subscriptions.reactivate(tenantId);\nawait subscriptions.subscriptions.renew(tenantId);\n```\n\nUseful helpers:\n\n- `get(subscriberId)`\n- `previewChangePlan(subscriberId, newPlanId)`\n- `pause(subscriberId)`\n- `resume(subscriberId)`\n- `reactivate(subscriberId)`\n- `startTrial(subscriberId, planId, days)`\n- `extendTrial(subscriberId, days)`\n- `isActive(subscriberId)`\n- `isTrialing(subscriberId)`\n- `daysRemaining(subscriberId)`\n\n### Permissions and Usage\n\n```ts\nawait subscriptions.permissions.assertCan(tenantId, \"customDomain\");\nawait subscriptions.permissions.assertCanUse(tenantId, \"maxProducts\", 5);\n\nconst allFeatures = await subscriptions.permissions.getFeatures(tenantId);\nconst allUsage = await subscriptions.permissions.getAllUsage(tenantId);\n\nawait subscriptions.permissions.setUsage(tenantId, \"maxProducts\", 42);\nawait subscriptions.permissions.resetUsage(tenantId, \"maxProducts\");\n```\n\n### Webhooks\n\nWebhook handlers accept raw payloads as `string | Uint8Array`.\n\n```ts\nconst event = await subscriptions.handleWebhook(\n  \"moyasar\",\n  rawBody,\n  signature,\n);\n```\n\nThis works in Node.js, Bun, and edge-style runtimes as long as you preserve the raw request body.\n\n### Invoices\n\n```ts\nconst invoice = await subscriptions.invoices.create({\n  subscriptionId: subscription.id,\n  amount: 49,\n  currency: \"USD\",\n  status: \"paid\",\n  gatewayInvoiceId: \"pay_123\",\n  lineItems: [\n    {\n      description: \"Pro monthly subscription\",\n      quantity: 1,\n      unitPrice: 49,\n      amount: 49,\n    },\n  ],\n});\n\nconst detailed = await subscriptions.invoices.getWithDetails(invoice.id);\n```\n\nInvoice HTML rendering and PDF generation are exported from the package root. PDF generation is intended for Node.js environments.\n\n## Elysia Integration\n\nThe package exports `elysiaPlugin` from `@abshahin/subscriptions/elysia`.\n\n```ts\nimport { Elysia } from \"elysia\";\nimport { elysiaPlugin } from \"@abshahin/subscriptions/elysia\";\nimport { subscriptions } from \"./subscriptions\";\n\nconst app = new Elysia().use(\n  elysiaPlugin(subscriptions, {\n    prefix: \"/subscriptions\",\n    getSubscriberId: (ctx) =\u003e ctx.user.activeTenantId,\n    adminRoutes: true,\n    adminGuard: (ctx) =\u003e ctx.user.role === \"admin\",\n    invoice: {\n      platform: {\n        name: \"Manhali\",\n        website: \"https://example.com\",\n        supportEmail: \"support@example.com\",\n      },\n      locale: \"ar-EG\",\n      getSubscriberInfo: async (subscriberId) =\u003e ({\n        name: `Tenant ${subscriberId}`,\n      }),\n    },\n  }),\n);\n```\n\nBuilt-in routes include:\n\n- `GET /current`\n- `GET /plans`\n- `POST /subscribe`\n- `POST /create`\n- `POST /change-plan`\n- `GET /change-plan/preview/:planId`\n- `POST /cancel`\n- `POST /resume`\n- `POST /reactivate`\n- `GET /features`\n- `GET /usage`\n- `GET /usage/:feature`\n- `GET /can/:feature`\n- `GET /invoices`\n- `GET /invoices/:id/download`\n- `POST /webhooks/:provider`\n\nIt also adds route macros for controller-level enforcement:\n\n```ts\napp.get(\"/analytics\", handler, {\n  requireFeature: \"analyticsEnabled\",\n});\n\napp.post(\"/products\", handler, {\n  requireUsage: { feature: \"maxProducts\", count: 1 },\n});\n\napp.post(\"/products\", handler, {\n  useFeature: \"maxProducts\",\n});\n```\n\nIf you enable invoice downloads through the Elysia plugin, run that endpoint on Node.js and install `puppeteer-html-pdf`.\n\n## Payments\n\nPayments are optional. If no payment adapter is configured, the package still supports manual subscription management.\n\nFor Moyasar:\n\n```ts\nimport { moyasarAdapter } from \"@abshahin/subscriptions/adapters/moyasar\";\n\nconst payment = moyasarAdapter({\n  secretKey: process.env.MOYASAR_SECRET_KEY!,\n  publishableKey: process.env.MOYASAR_PUBLIC_KEY!,\n  webhookSecret: process.env.MOYASAR_WEBHOOK_SECRET,\n  callbackUrl: \"https://app.example.com/subscription\",\n});\n```\n\nThe backend project currently uses direct payment charges plus saved token IDs for renewals and plan upgrades. That pattern is covered in the integration guide.\n\n## Prisma Schema Requirements\n\nThe package expects four core models:\n\n- `SubscriptionPlan`\n- `Subscription`\n- `Invoice`\n- `UsageRecord`\n\nSee `docs/prisma-schema.md` for a schema example based on the backend project.\n\n## What Stays Outside This Package\n\nThe backend project uses this package as the subscription source of truth, but keeps a few concerns in app code:\n\n- Redis-backed hot-path usage counters\n- cron-based renewal orchestration\n- tenant-aware cache invalidation across the broader app\n- payment verification callbacks specific to the frontend flow\n\nThat split is intentional. This package owns subscription state and policy. Your application can add faster counters, schedulers, and dashboards around it.\n\n## Type Safety\n\nThe package provides full type inference for features:\n\n```ts\nconst features = defineFeatures({\n  analytics: { type: \"boolean\", default: false },\n  maxProducts: { type: \"limit\", default: 100 },\n});\n\nawait subs.can(tenantId, \"analytics\");\nawait subs.permissions.getFeatures(tenantId);\n```\n\n## Documentation\n\n- `CHANGELOG.md`\n- `docs/README.md`\n- `docs/adapters.md`\n- `docs/error-handling.md`\n- `docs/integration-guide.md`\n- `docs/prisma-schema.md`\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faashahin%2Fsubscriptions-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faashahin%2Fsubscriptions-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faashahin%2Fsubscriptions-sdk/lists"}