Stripeflare is middleware that adds Stripe Payments to a Cloudflare Worker and have users keep track of a balance in your own database, without requiring third-party authentication (Just Stripe Payment)! Let me know your thoughts in this thread and check the demo
⚡️ Lightning Fast | 🔑 ACID Compliant | ☁️ Cloudflare Optimised | 🍬 Minimal Setup
Stripeflare is meant to make it easy to create Agent-friendly SaaS products.
Aspect | Traditional SaaS | Agent-Friendly SaaS |
---|---|---|
Authentication | Username/password, OAuth, SSO | Payment-based authentication, API keys |
User Journey | Sign up → Trial → Subscribe → Use | Pay per use → Immediate access |
Billing Model | Monthly/annual subscriptions | Pay-per-request, usage-based |
User Interface | UI-First | 1:1 JSON API, Markdown API & UI |
npx create-stripeflare
See create-stripeflare for more detailed instructions.
First install the package (which also installs DORM)
npm i stripeflare
The easiest way to install stripeflare for existing projects, is to use withStripeflare
which wraps your entire ExportedHandler
:
import { withStripeflare, StripeUser, DORM } from "stripeflare";
export { DORM };
type Env = {};
export default {
// StripeUser can be extended
fetch: withStripeflare<Env, StripeUser>(
async (request, env, ctx) => {
// ctx.user, ctx.client are now available
const { user, registered } = ctx;
if (request.url.endsWith("/charge")) {
if (user.balance < 1) {
return new Response("Payment Required", { status: 402 });
}
return new Response(JSON.stringify(result), {
headers: {
// Charges one cent
"X-Price": "1",
},
});
}
return new Response(`Hello ${user.name || "Anonymous"}`);
},
{
// customMigrations: optional way to overwrite default user table with an extension to your database
// version: "1" // resets data
},
),
} satisfies ExportedHandler<Env>;
wrangler.json|toml
The following adds DORM durable object to your Cloudflare Bindings, which stripeflare uses to allow for lightning-fast user balance charges.
{
"name": "stripeflare-example",
"compatibility_date": "2025-06-06",
"main": "main.ts",
"durable_objects": {
"bindings": [{ "name": "DORM_NAMESPACE", "class_name": "DORM" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["DORM"] }]
}
For TOML:
name = "stripeflare-example"
main = "main.ts"
compatibility_date = "2025-06-06"
[[durable_objects.bindings]]
name = "DORM_NAMESPACE"
class_name = "DORM"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["DORM"]
.dev.vars
# Create a Stripe account, navigate to https://dashboard.stripe.com/apikeys and collect `STRIPE_SECRET` and `STRIPE_PUBLISHABLE_KEY`
STRIPE_SECRET=
STRIPE_PUBLISHABLE_KEY=
# Create a webhook at https://dashboard.stripe.com/webhooks/create. Endpoint URL: https://yourdomain.com/stripe-webhook and sollect `STRIPE_WEBHOOK_SIGNING_SECRET`
STRIPE_WEBHOOK_SIGNING_SECRET=
# Create a payment link at https://dashboard.stripe.com/payment-links and set this as `STRIPE_PAYMENT_LINK`
STRIPE_PAYMENT_LINK=
# Put a 32 character+ secret here to sign your secrets
DB_SECRET=
withStripeflare
passes the follwoing to your fetch
handler:
-
ctx.charge(amount, allowNegativeBalance)
charges the user -
ctx.user
has a user object with or without details -
ctx.client
contains your db client
In other handlers (schedules, queue, etc) you can use the chargeUser
utility function to easily charge a user given their access_token:
const chargeUser = async (
env: Env,
ctx: ExecutionContext,
user_access_token: string,
migrations: any | undefined,
version: string | undefined,
amountCent: number,
allowNegativeBalance: boolean,
) => Promise<{ charged: boolean; message: string }>;
In your static files you can also access these:
-
GET /me
returns{paymentLink,client_reference_id, balance, name, email, card_fingerprint, verified_email }
which can be useful to show these details fast. Requires cookie! -
POST /rotate-token
(to rotate the token, instable, may be removed in future version) -
/db/*
(to perform direct queries to your databases) - for DORM integration with outerbase, see DORM Docs
Besides that, stripeflare standardises and uses the following response headers:
- You can pass
X-Price
to charge users automatically (amount in cents) - Stripeflare passes
X-PaymentLink
with the paymentlink for the user - Stripeflare passes
X-Balance
with the balance of the current user in cents
- Performant: Creates a DB for each user while also mirroring it into an aggregate db (powered by Durable Objects and DORM), resulting in lightning-fast worker requests with user-balance.
-
Flexible: Leverages
?client_reference_id
ensure connection to the user session whilst using any Stripe Payment Link. - Extensible: Hooks into your own DO-based database so you can extend it however you like.
-
Login by Payment: Users can access their previous balance from any device as long as they use the same Stripe Payment method (only supports payment methods
card
andlink
, see ADR)
- You want to use Cloudflare Workers for your app, with DORM as your database with segmentation on the user-level with one aggregate-db
- You are VERY concerned with the performance of charging users. A user should be able to be charged within ±20ms.
- You can use the source code as a starting point, giving you a virtual wallet system for your DORM-database. You don't need to use it as a dependency if you need additional logic and enhanced security.
- When you care a lot about multiple layers of security, don't use this. Currently, access_tokens are stored in the database as-is without encryption, which could expose them if other layers of security are compromised.
- When you want more production-ready things, don't use this. May still get breaking changes. This is still a research-project with limitations. See ADR for details.
-
https://github.com/janwilmake/dorm is a dependency. When you want to interact with the same database to charge a user, ensure to use the returned
userClient
from the middleware (if available) or create a client yourself. The DB name access_token, the mirrorName should be "aggregate" - we have some well-thoguht-out logic in the stripe webhook, allowing login-by-payment; read more here: https://www.lmpify.com/httpsrawgithubus-20o3gj0
- learn how to explore the data with outerbase here: https://www.lmpify.com/how-does-stripeflare-iqt4li0
- It is a massive improvement upon Cloudflare Sponsorware which I made before as it dramatically reduces complexity while improving upon UX and DX.
Stripeflare uses DORM for database management and supports custom migrations to extend the user schema beyond the default fields.
The users
table must contain these required columns (all should be indexed for performance):
CREATE TABLE users (
access_token TEXT PRIMARY KEY, -- Required: User's session token
balance INTEGER DEFAULT 0, -- Required: User's balance in cents
name TEXT, -- Required: User's display name
email TEXT, -- Required: User's email address
verified_email TEXT, -- Required: Verified email for login-by-payment
verified_user_access_token TEXT, -- Required: Links to verified user sessions
card_fingerprint TEXT, -- Required: Stripe card fingerprint for user matching
client_reference_id TEXT -- Required: Encrypted reference for Stripe integration
);
Stripeflare includes these default migrations:
const defaultMigrations = {
1: [
`CREATE TABLE users (
access_token TEXT PRIMARY KEY,
balance INTEGER DEFAULT 0,
name TEXT,
email TEXT,
verified_email TEXT,
verified_user_access_token TEXT,
card_fingerprint TEXT,
client_reference_id TEXT
)`,
`CREATE INDEX idx_users_balance ON users(balance)`,
`CREATE INDEX idx_users_name ON users(name)`,
`CREATE INDEX idx_users_email ON users(email)`,
`CREATE INDEX idx_users_verified_email ON users(verified_email)`,
`CREATE INDEX idx_users_card_fingerprint ON users(card_fingerprint)`,
`CREATE INDEX idx_users_client_reference_id ON users(client_reference_id)`,
],
};
You can extend the schema by providing custom migrations that include additional columns:
import { withStripeflare, StripeUser } from "stripeflare";
// Extend the StripeUser interface
interface ExtendedUser extends StripeUser {
subscription_tier: string;
created_at: string;
last_login: string;
preferences: string; // JSON string
}
// Define custom migrations that include all required fields PLUS your extensions
const customMigrations = {
1: [
// Base users table with ALL required fields + your custom fields
`CREATE TABLE users (
access_token TEXT PRIMARY KEY,
balance INTEGER DEFAULT 0,
name TEXT,
email TEXT,
verified_email TEXT,
verified_user_access_token TEXT,
card_fingerprint TEXT,
client_reference_id TEXT,
-- Your custom fields below
subscription_tier TEXT DEFAULT 'free',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_login TEXT,
preferences TEXT DEFAULT '{}'
)`,
// Required indexes (don't remove these)
`CREATE INDEX idx_users_balance ON users(balance)`,
`CREATE INDEX idx_users_name ON users(name)`,
`CREATE INDEX idx_users_email ON users(email)`,
`CREATE INDEX idx_users_verified_email ON users(verified_email)`,
`CREATE INDEX idx_users_card_fingerprint ON users(card_fingerprint)`,
`CREATE INDEX idx_users_client_reference_id ON users(client_reference_id)`,
// Your custom indexes
`CREATE INDEX idx_users_subscription_tier ON users(subscription_tier)`,
`CREATE INDEX idx_users_created_at ON users(created_at)`,
],
// Future migrations for schema changes
2: [
`ALTER TABLE users ADD COLUMN api_key TEXT`,
`CREATE INDEX idx_users_api_key ON users(api_key)`,
],
};
type Env = {};
export default {
fetch: withStripeflare<Env, ExtendedUser>(
async (request, env, ctx) => {
const { user, client } = ctx;
// Access your custom fields
console.log(`User tier: ${user.subscription_tier}`);
// Update custom fields
if (client) {
await client
.exec(
"UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE access_token = ?",
user.access_token,
)
.toArray();
}
return new Response(`Hello ${user.name}!`);
},
{
customMigrations,
version: "1", // Increment to reset/migrate data
},
),
};
- Always include required fields: Never remove the required columns from your migrations
- Keep required indexes: The default indexes are essential for performance
// Type-safe access to extended fields
const { user, client } = ctx;
// Read extended data
const preferences = JSON.parse(user.preferences || "{}");
// Update extended data
if (client) {
await client
.exec(
"UPDATE users SET subscription_tier = ?, preferences = ? WHERE access_token = ?",
"premium",
JSON.stringify({ theme: "dark", notifications: true }),
user.access_token,
)
.toArray();
}
Stripeflare creates:
- Individual user databases: One Durable Object per user (fast charging)
- Aggregate database: Mirrors all user data for admin queries
- Automatic mirroring: Changes sync between user DB and aggregate DB
This architecture ensures lightning-fast user operations while maintaining queryable aggregate data.
- Required fields: Cannot be removed or renamed
-
Primary key:
access_token
must remain the primary key -
Balance type: Must remain
INTEGER
(cents) - Token rotation: Custom fields are preserved during token rotation