Skip to content

Latest commit

 

History

28 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tenon — Backend

Feature entitlement API for SaaS. Plan-based access control, usage tracking, and billing infrastructure — without the complexity.


Overview

SaaS companies routinely waste engineering cycles hardcoding plan limits and rebuilding billing logic every time pricing changes. Tenon decouples monetization from core business logic. Your payment platform handles the money. Tenon handles what each customer is allowed to do.

Integrate once and get usage tracking, rate limiting, and feature gating out of the box — works with whatever payment provider you already use. Built for SaaS products that need plan-based access control without rebuilding it from scratch.

This repository covers the entire backend API. A separate Next.js frontend handles the Tenon dashboard.


Stack

Layer Technology
Runtime Node.js + TypeScript
Framework Express 5
Database PostgreSQL
ORM Drizzle ORM
Auth JWT (access + refresh) + token versioning
Payments Stripe
Email Nodemailer
Security Helmet, bcrypt

Setup

Prerequisites

  • Node.js >= 18
  • PostgreSQL database
  • Stripe account with webhook signing secret
  • A .env file with the variables listed below

Install

git clone https://github.com/OgheneDev/billing-engine.git
cd billing-engine
npm install

Environment Variables

# Server
PORT=3000
NODE_ENV=development

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/tenon

# Auth
JWT_SECRET=
JWT_REFRESH_SECRET=
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# Email (Nodemailer)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
FROM_EMAIL=no-reply@yourdomain.com

# App
FRONTEND_URL=http://localhost:3001

Database

# Push schema to database
npm run db:push

# Or generate and run migrations
npm run db:generate

Run

# Development (with watch)
npm run dev

# Production
npm run build
npm start

Database Schema

All tables use UUID primary keys. The schema is defined in src/db/schema.ts using Drizzle ORM.

users

Tenon platform users — the people who log into the Tenon dashboard to manage their org.

Column Type Notes
id uuid PK
email varchar(255) Unique, indexed
password_hash varchar(255) bcrypt hashed
name varchar(255)
email_verified boolean
token_version integer Incremented on logout/password change to invalidate all sessions
auth_provider varchar(50) Default password
reset_password_token text
reset_password_expires timestamp
last_login_at timestamp
last_password_change timestamp

organizations

The top-level tenancy unit. Represents a Tenon customer — a SaaS company using the API.

Column Type Notes
id uuid PK
name varchar(255)
slug varchar(100) Unique, indexed
owner_user_id uuid FK → users
billing_email varchar(255)
stripe_customer_id varchar(255) Unique
max_seats integer Default 5
deleted_at timestamp Soft delete

organization_members

Junction table placing users inside orgs with a role. Tracks invitation lifecycle.

Column Type Notes
organization_id uuid FK → organizations
user_id uuid FK → users
role varchar(50) owner | admin | member
invited_by uuid FK → users
status varchar(30) invited | active
joined_at timestamp

Indexed on (organization_id, user_id) — unique pair.

plans

Platform-level plan definitions. These are Tenon's own billing plans (what orgs pay to use Tenon).

Column Type Notes
id uuid PK
key varchar(100) Unique — used as the stable plan identifier
name varchar(255)
billing_interval varchar(20) month | year
base_price integer In cents
currency varchar(3) Default usd
is_metered boolean

plan_features

Feature limits attached to each plan. Stored as (plan_key, feature_key, limit) rows — adding a feature to a plan is a data operation, not a code change.

Column Type Notes
plan_key varchar(100) FK → plans.key
feature_key varchar(100) e.g. api_calls, exports, seats
limit integer null = unlimited

Unique on (plan_key, feature_key).

subscriptions

Stripe subscription state mirrored locally. Fully driven by Stripe webhooks — never polled.

Column Type Notes
organization_id uuid FK → organizations
stripe_subscription_id varchar(255) Unique
stripe_price_id varchar(255)
stripe_product_id varchar(255)
status varchar(50) Mirrors Stripe status
plan_key varchar(100)
plan_snapshot text JSON — plan state at time of subscription
seat_limit integer
current_period_start timestamp
current_period_end timestamp
cancel_at_period_end boolean
grace_period_ends_at timestamp
trial_start / trial_end timestamp

invoices

Stripe invoice records mirrored locally. Written on invoice.paid webhook events.

Column Type Notes
organization_id uuid FK → organizations
subscription_id uuid FK → subscriptions
customer_id uuid FK → customers
stripe_invoice_id varchar(255) Unique
amount_due integer In cents
amount_paid integer
tax integer
status varchar(50)
hosted_invoice_url varchar(500)
invoice_pdf varchar(500)
paid_at timestamp

payments

Individual payment records linked to invoices.

Column Type Notes
invoice_id uuid FK → invoices
provider varchar(50) e.g. stripe
provider_payment_id varchar(255)
status varchar(50)
amount integer In cents
currency varchar(3)

customer_plans

Org-defined plans for their end-customers. This is the core Tenon product — orgs create plans here that gate access in their own SaaS.

Column Type Notes
organization_id uuid FK → organizations
key varchar(100) Stable plan identifier
name varchar(255)
features text JSON — { "api_calls": 1000, "exports": 10, "advanced": null }. null = unlimited, absent key = blocked
is_active boolean

Unique on (organization_id, key).

customers

End-customers of the org — the users of the org's own SaaS product. Tenon never needs to be their system of record.

Column Type Notes
organization_id uuid FK → organizations
external_id varchar(255) The org's own ID for this user. Unique per org — Tenon stores no PII it doesn't need
email varchar(255) Optional
plan_key varchar(100) FK → customer_plans.key
status varchar(50) Default active
metadata text Arbitrary JSON the org wants to store
current_period_start timestamp Resets on plan change

Unique on (organization_id, external_id).

usage_events

Immutable event log. Written every time a customer performs a metered action. Idempotent by (organization_id, idempotency_key).

Column Type Notes
organization_id uuid FK → organizations
subscription_id uuid FK → subscriptions
customer_id uuid FK → customers
event_type varchar(100) e.g. api_call, export
quantity integer Default 1
idempotency_key varchar(255) Prevents double-counts
reported_to_stripe boolean Async Stripe metered billing sync
stripe_usage_record_id varchar(255)
metadata text

Indexed on (organization_id), (timestamp), and (organization_id, event_type).

usage_aggregates

Pre-aggregated usage totals per org per billing period per metric. Written by a background job — avoids full usage_events scans on every entitlement check.

Column Type Notes
organization_id uuid FK → organizations
metric varchar(100)
period_start timestamp
period_end timestamp
total integer

Unique on (organization_id, metric, period_start, period_end).

usage_alerts

Threshold alerts that fire when a customer's usage for a feature reaches a set percentage of their plan limit.

Column Type Notes
organization_id uuid FK → organizations
customer_id uuid FK → customers
feature varchar(100)
threshold integer 0–100. Fires when usage % hits this
fired boolean
fired_at timestamp

Unique on (customer_id, feature).

webhook_events

Idempotency and retry log for incoming Stripe webhooks. Every event is recorded before processing — duplicate delivery is safe.

Column Type Notes
event_id varchar(255) Unique — Stripe event ID
event_type varchar(255)
processed boolean
processing_error text
attempts integer
next_retry_at timestamp
idempotency_key varchar(255)
provider varchar(50) Default stripe
data text Raw event payload
processed_at timestamp

api_keys

API keys issued to orgs for authenticating requests to the public V1 API. Stored as SHA-256 hashes — the raw key is shown once at creation.

Column Type Notes
organization_id uuid FK → organizations
hashed_key varchar(64) SHA-256, never stored in plaintext
last_used_at timestamp
revoked_at timestamp Null = active

failed_login_attempts

Security telemetry for brute-force detection. Indexed on email, user ID, and timestamp for fast windowed queries.

Column Type Notes
user_id uuid FK → users (nullable — tracks attempts on unknown emails too)
email varchar(255)
ip_address varchar(45) IPv6-safe
reason varchar(100)

security_alerts

Generated internally when anomalous patterns are detected (brute force, multi-IP auth, etc.). Resolvable by org admins.

Column Type Notes
organization_id uuid FK → organizations
user_id uuid FK → users
type varchar(100)
severity varchar(20)
title varchar(255)
description text
metadata text
resolved boolean
resolved_by uuid FK → users
audit_log_ids uuid[] Array of linked audit log entries

audit_logs

Security event log. Non-blocking async writes — never in the critical path.

Column Type Notes
organization_id uuid FK → organizations
user_id uuid FK → users
action varchar(100) e.g. plan.created, customer.plan_changed
resource_type varchar(50)
resource_id uuid
severity varchar(20) Default info
ip_address varchar(45)
details text

Indexed on (organization_id), (timestamp), and (action).


Architecture

Two-Layer Tenancy

Tenon has a deliberate two-layer model that's worth understanding upfront:

  • Orgs — companies that pay Tenon to use the platform. They log into the dashboard, create plans, and call the API.
  • Customers — the end-users of those orgs' own SaaS products. Tenon never requires their PII; they're referenced by external_id — whatever ID the org already uses in their own system.

This separation means an org can integrate Tenon without migrating their user table or exposing user data to a third party.

Auth

JWT access tokens (short-lived) + refresh tokens (long-lived, HTTP-only cookies). token_version on users is incremented on logout and password change, atomically invalidating all active sessions without maintaining a token blocklist.

API Key Design

Public V1 API keys use a tn_ prefix and are stored as SHA-256 hashes — the pattern mirrors Stripe's sk_ model. The plaintext key is shown exactly once at creation. Revocation is a single revoked_at timestamp write.

Dual API Surface

Two route namespaces with different stability guarantees:

  • /api/* — internal dashboard API. Can evolve freely alongside the frontend.
  • /api/v1/* — public integration API consumed by orgs' own backends. Versioned for stability.

Feature Gating

Feature limits live in plan_features rows, not code. Checking entitlement at request time reads from the org's active plan — adding or modifying limits requires no redeployment. The customer_plans.features JSON column uses a deliberate encoding: null means unlimited, an absent key means the feature is blocked entirely.

Stripe Integration

Subscription state is fully driven by Stripe webhooks — no polling. Every incoming event is written to webhook_events before processing, making duplicate delivery idempotent. The processed flag and attempts counter support retry logic for transient failures.

Usage Tracking

Usage events are written to usage_events with an idempotency_key — a unique constraint on (organization_id, idempotency_key) prevents double-counts at the database level regardless of retry behavior. Events are tracked locally before async reporting to Stripe for metered billing, decoupling the critical path from Stripe latency.

usage_aggregates stores pre-rolled totals per billing period so entitlement checks read a single row rather than summing the full event log.

Security Monitoring

Brute-force and multi-IP anomaly detection runs against failed_login_attempts without any external SIEM dependency. Detected patterns generate security_alerts that org admins can investigate and resolve. All security-relevant actions write to audit_logs asynchronously — fire-and-forget, never blocking the response.


API Structure

/auth              Registration, login, refresh, logout, password reset, email verification
/users             Profile management
/orgs              Org creation, member management, seat management, settings
/plans             Platform plan listing
/subscriptions     Stripe subscription management, portal, status
/api-keys          Key creation, listing, revocation
/customers         End-customer CRUD, plan assignment
/customer-plans    Org-defined plan creation and management
/usage             Event ingestion, aggregation queries, threshold alerts
/entitlements      Feature gate checks — the core integration endpoint
/webhooks          Stripe webhook receiver
/invoices          Invoice listing and detail
/security          Alert listing, resolution, audit log access

Project Context

  • Role: Solo — full backend and frontend.
  • Time invested: ~39h backend, ~17h frontend.
  • Status: Live at tenon-org.vercel.app.

Author

Emmanuel Oghene — Fullstack Developer, Lagos Portfolio · GitHub · LinkedIn

About

Feature entitlement API for SaaS. Plan-based access control, usage tracking, and billing infrastructure, without the complexity.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages