This repository contains a complete Backend-as-a-Service (BAAS) for software license management, built to run on Cloudflare Workers. It features strong device binding via WebAuthn, usage analytics, and flexible request quotas.
- Simplicity: The developer (you) owns the UI, local storage strategy (beyond the SDK's caching), and multi-user flow. The BAAS handles the core backend tasks: licensing, device binding, quotas, and analytics.
- Non-transferable Licenses: Strong device binding is enforced via WebAuthn on first activation, making licenses difficult to transfer.
- Flexible Plans: Supports duration-based licenses with configurable request quotas (daily, monthly, RPM).
- Agency Model: Allows for issuing licenses to an agency with a specific number of seats, with analytics rolling up to the agency level.
- Privacy-Focused: No application content is ever collected. The service only tracks usage counts and hashed user identifiers.
.
├── apps/
│ ├── baas-api/worker.js # Cloudflare Worker Edge API (D1 SQLite)
│ └── dashboard/index.html # Minimal admin UI
├── db/
│ └── schema.sql # D1 database schema
├── sdk/
│ └── baas-sdk.js # Headless-first, paste-in SDK
├── wrangler.toml # Cloudflare config (D1 + vars + secrets)
└── README.md # This file
-
Clone the Repository: Get the code on your local machine.
-
Install Wrangler CLI: If you don't have it, install the Cloudflare command-line tool:
npm install -g wrangler
-
Create a D1 Database:
wrangler d1 create baas-db
This command will output the database ID. Open
wrangler.tomland replaceyour-d1-database-idwith this ID. -
Apply the Database Schema:
wrangler d1 execute baas-db --file=./db/schema.sql
-
Set Secrets: You need to configure three secrets for the worker. Generate these values and set them using the
wrangler secret putcommand.-
ADMIN_SECRET: A secret for accessing the admin API.# Generate a secret openssl rand -base64 32 # Set the secret (replace <value> with the generated string) wrangler secret put ADMIN_SECRET
-
PRIVATE_JWK&PUBLIC_JWK_JSON: An RS256 key pair for signing and verifying JWTs. You can use a library likejosein Node.js to generate these.- The
PRIVATE_JWKis the full private key, stringified. - The
PUBLIC_JWK_JSONis the public part of the key, stringified.
# Example of setting the secrets wrangler secret put PRIVATE_JWK -- '{"kty":"RSA","n":"...","e":"AQAB","d":"..."}' wrangler secret put PUBLIC_JWK_JSON -- '{"kty":"RSA","n":"...","e":"AQAB"}'
- The
-
-
Update
wrangler.toml:- Set
PUBLIC_BASE_URLto the URL where your worker will be deployed. - Set
ALLOWED_ORIGINSto a comma-separated list of domains where your application will run.
- Set
-
Update the SDK:
- Open
sdk/baas-sdk.js. - Replace the placeholder
PUBLIC_JWKwith the actual public key you generated. - Replace the placeholder
apiBasewith your worker's public URL.
- Open
-
Publish the Worker:
wrangler deploy
-
Use the Dashboard: Open
apps/dashboard/index.htmlin your browser. Enter your worker URL and admin secret to start managing your licenses.
The SDK is designed to be headless-first. You can integrate it into your application with just a few lines of code.
<script src="path/to/baas-sdk.js"></script>
<script>
// 1. Ensure the license is active
BAAS.license.ensure({
// This function is called when the SDK needs a license token.
// You should fetch this from your backend, which authenticates the user.
onRequestLicense: async () => {
// Example: return "ey...your.license.token";
const res = await fetch('/api/get-license-token');
const { token } = await res.json();
return token;
},
// Optional: Get status updates
onStatus: (status) => console.log('BAAS Status:', status)
});
// 2. (Optional) Provide a user identifier for analytics
// The SDK will hash this value, so the raw ID is never sent.
BAAS.context.setUserProvider(() => {
// Return the current user's unique ID, or null/undefined if logged out
return getCurrentUserId();
});
// 3. Meter usage with wrappers or direct calls
// Wrap a function to meter its execution
const myExpensiveFunction = BAAS.quota.wrap('myFunc', () => {
console.log('Doing expensive work!');
}, { bucket: 'ai', cost: 5 });
// Use the metered fetch wrapper
BAAS.quota.fetch('https://api.example.com/data', {}, { bucket: 'api-calls' });
</script>- Heartbeat: The SDK sends a heartbeat every 60 seconds with the
activationCertand an optional hasheduserId. This updates thelast_seentimestamp for the device and logs the user hash for unique user counts. - Usage Metering: Every call to
BAAS.quota.meteror a wrapped function sends a record to the backend, incrementing usage in a named bucket. - Privacy: All analytics are based on counts, device IDs, and hashed user IDs. No PII or application content is collected.
Quotas are defined in a JSON object when you issue a license. They can be global or per-bucket.
{
"daily": 5000, // Global daily limit
"byCategory": {
"ai": { "daily": 100 }, // Limit for the 'ai' bucket
"general": { "daily": 4900 } // Limit for the 'general' bucket
}
}If a metered request exceeds a quota, the API returns a 429 Too Many Requests error, and the SDK throws an error containing the limit information.
- WebAuthn: Device binding is handled by the browser's WebAuthn API, creating a strong, non-transferable link between a license and a device.
- JWTs: License tokens and activation certificates are signed with RS256. The public key is embedded in the SDK for client-side verification of license tokens.
- No Content Collection: The BAAS is fundamentally unaware of your application's content.
The admin dashboard (apps/dashboard/index.html) is a simple, self-contained HTML file that provides a user interface for managing your BAAS instance.
-
Configuration:
- API Base URL: This is the full URL of your deployed Cloudflare Worker. For example:
https://baas-api.your-worker-name.workers.dev. - Admin Secret: This is the secret you created and set using
wrangler secret put ADMIN_SECRET.
- API Base URL: This is the full URL of your deployed Cloudflare Worker. For example:
-
Create Agency:
- This form allows you to create a new agency.
- Agency ID: A unique, URL-friendly identifier for the agency (e.g.,
acme-corp). - Agency Name: The full, human-readable name (e.g., "Acme Corporation").
- Seat Quota: The maximum number of licenses that can be issued to this agency.
-
Issue Licenses:
- This is where you create new licenses.
- Project ID: An identifier for your application (e.g.,
my-awesome-app). - Type: Use
seatfor licenses that belong to an agency, orindividualfor standalone licenses. - Agency ID: If the type is
seat, provide theagencyIdthis license belongs to. - Number of Seats: The number of licenses to create with these settings.
- Duration (Days): The number of days the license will be valid for after activation.
- Quotas (JSON): A JSON object defining the API usage limits.
-
Revoke License:
- This allows you to disable a license.
- License JTI to Revoke: The unique
jti(JWT ID) of the license you want to revoke. You can get this from the response when you issue a license.
-
View Agency Summary:
- This provides analytics for a specific agency.
- Agency ID: The ID of the agency you want to view.
The BAAS is "headless" and does not have built-in plan names like "Free" or "Pro". Instead, it provides the flexibility to create any kind of plan you want using Duration and Quotas. You define what a "Pro" plan means when you issue the license.
Here’s how you would create different tiers using the Issue Licenses form:
-
To Create a "Free" Plan:
- Duration (Days): Set a short duration, like
14. - Quotas (JSON): Set low API limits.
{ "daily": 100 }
- Duration (Days): Set a short duration, like
-
To Create a "Basic" Plan:
- Duration (Days): Set a longer duration, like
365. - Quotas (JSON): Set higher limits.
{ "daily": 1000 }
- Duration (Days): Set a longer duration, like
-
To Create an "Advanced/Pro" Plan:
- Duration (Days):
365 - Quotas (JSON): Set very high limits and use different buckets for different features. For example, you could give users 10,000 general requests but only 100 special "AI" requests per day.
{ "daily": 10000, "byCategory": { "ai": { "daily": 100 }, "general": { "daily": 9900 } } }
- Duration (Days):
You would handle the logic for which user gets which plan in your main application's backend, and then call this admin API to issue the license with the correct settings.
- 403 Forbidden Error:
- Check that your
ADMIN_SECRETis correct. - Ensure the
X-Admin-Secretheader is being sent correctly from the dashboard.
- Check that your
- "Invalid JWT" or "Token verification failed":
- Make sure the
PUBLIC_JWK_JSONsecret is set correctly in your worker and that thePUBLIC_JWKinsdk/baas-sdk.jsmatches.
- Make sure the
- 429 Too Many Requests:
- This means a quota has been exceeded. Check the
apiResponsein the dashboard for details on which limit was hit.
- This means a quota has been exceeded. Check the
- CORS Errors:
- Ensure that the
ALLOWED_ORIGINSvariable in yourwrangler.tomlincludes the domain where you are running your application (and the dashboard, if you are running it from a different origin).
- Ensure that the