Processing Stripe Payments
Written by Basile Samel.
Published Oct 11, 2024. Last updated Oct 11, 2024.
Here is how you create subscriptions or one-time payment with Stripe and a simple hono.js
web server.
1. Database Service
import Database from "better-sqlite3";
import fs from "node:fs/promises"
let db_singleton = false
const path = `${import.meta.dirname}/../../db/database.db`;
export function db () {
if (!db_singleton) {
db_singleton = new Database(path);
}
return db_singleton;
}
export function destroyDatabase(){
return fs.rm(path)
}
export default db
CREATE TABLE user (
id TEXT NOT NULL PRIMARY KEY,
github_id VARCHAR(255) UNIQUE,
username VARCHAR(255),
email VARCHAR(255),
avatar_url VARCHAR(255),
hasPaid BOOLEAN DEFAULT FALSE,
role VARCHAR(255) DEFAULT "USER"
);
CREATE TABLE IF NOT EXISTS
paymentSession (
id INTEGER PRIMARY KEY,
created_at BIGINT DEFAULT CURRENT_TIMESTAMP,
updated_at BIGINT DEFAULT CURRENT_TIMESTAMP,
uid VARCHAR(36) NOT NULL,
fk_user TEXT,
session_id TEXT,
FOREIGN KEY(fk_user) REFERENCES user(id)
);
"scripts": {
"dev": "node ./index.js",
"test": "node --test",
"db:create": "sqlite3 ./db/database.db < ./schema.sql",
"db:destroy": "rm ./db/database.db",
"db:recreate": "npm run db:destroy && npm run db:create"
},
2. User Service
cf auth article
3. Stripe Service
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
import dotenv from "dotenv";
dotenv.config();
import db from "./db.js";
export function createTable () {
return db().prepare(`
CREATE TABLE IF NOT EXISTS
paymentSession (
id INTEGER PRIMARY KEY,
created_at BIGINT DEFAULT CURRENT_TIMESTAMP,
updated_at BIGINT DEFAULT CURRENT_TIMESTAMP,
uid VARCHAR(36) NOT NULL,
fk_user TEXT,
session_id TEXT,
FOREIGN KEY(fk_user) REFERENCES user(id)
);
`)
.run()
}
export async function create({ user }){
const stripe = new Stripe(process.env.STRIPE_SK);
const uid = uuidv4()
const session = await stripe.checkout.sessions.create({
client_reference_id: uid,
line_items: [
{
price: process.env.STRIPE_PRICE_ID,
quantity: 1
}
],
mode: 'payment',
success_url: `https://devreel.app/studio?payment=1`,
cancel_url: `https://devreel.app/studio?payment=0`,
});
await db()
.prepare(`
INSERT INTO paymentSession (
uid,
session_id,
fk_user
) VALUES (
?,
?,
?
)
`)
.bind(
uid,
session.id,
user.id
)
.run();
return {
success: true,
uid,
session_url: session.url,
session_id: session.id
};
}
export async function findOneBySessionId(args){
const res = await db()
.prepare(
`
SELECT * FROM paymentSession WHERE session_id = ? LIMIT 1
`
)
.get(args.session_id);
return res;
}
import test, { before, after } from 'node:test'
import assert from 'node:assert'
import { destroyDatabase } from "../src/services/db.js"
import * as UserService from "../src/services/user.js"
import * as PaymentService from "../src/services/payment.js"
let user = false
let session_id = false
before(async () => {
await UserService.createTable()
await PaymentService.createTable()
user = await UserService.create({
github_id: "test",
username: "test",
email: "test",
avatar_url: "test"
})
});
test('create payment session', async (t) => {
const paymentSession = await PaymentService.create({ user });
assert(paymentSession.session_url)
assert(paymentSession.session_id)
session_id = paymentSession.session_id
})
test('update user', async (t) => {
const paymentSession = await PaymentService.findOneBySessionId({ session_id });
assert(paymentSession)
const u = await UserService.findOneById(paymentSession.fk_user)
await UserService.update({
...u,
hasPaid: 1
})
const updated_user = await UserService.findOneById(paymentSession.fk_user)
assert(updated_user.hasPaid)
})
after(async () => {
await destroyDatabase()
})
4. Hono Server
import { Hono } from 'hono'
import dotenv from "dotenv";
dotenv.config()
const app = new Hono()
export default app
5. Stripe Endpoint
import * as PaymentService from "./services/payment.js";
app.post('/payment/sessions', async ctx => {
const user = ctx.data.session.user
const paymentSession = await PaymentService.create({ user });
return ctx.json({
ok: true,
session_url: paymentSession.session_url
});
})
6. Stripe Webhook
app.post('/webhook/payment', async ctx => {
const STRIPE_SK = process.env.STRIPE_SK;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const body = await ctx.req.text();
const stripe = new Stripe(STRIPE_SK);
const sig = ctx.req.header("stripe-signature");
let event = false;
try {
event = await stripe.webhooks.constructEventAsync(body, sig, WEBHOOK_SECRET);
} catch (err) {
throw new Error(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case "checkout.session.completed":
try {
const stripeSession = await stripe.checkout.sessions.retrieve(event.data.object.id)
const paymentSession = await PaymentService.findOneBySessionId({ session_id: stripeSession.id });
if(!paymentSession) {
throw new Error("Payment session not found");
}
const user = await UserService.findOneById(paymentSession.fk_user)
await UserService.update({
...user,
hasPaid: 1
})
} catch (err) {
console.log(err);
}
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
return ctx.json({
ok: true
}, 201)
})
7. Live testing
import { serve } from '@hono/node-server'
import app from "./src/index.js"
serve(app)
- Run server:
npm run dev
- Create Stripe tunnel
stripe login
stripe listen --forward-to localhost:8788/webhook/payment
-
Enter the webhook secret in your
.env
file -
Simulate event and make sure everything returns a 200 status code
stripe trigger payment_intent.succeeded
-
Use a Stripe payment link in test mode (the event will be streamed via tunnel to your localhost), e.g https://buy.stripe.com/test_4gwfZE8AJcmY0I89AA
-
Use the card 4242 4242 4242 4242 for testing (any CVC and future date)
-
Check the email inbox for the license key
Susbcribe to the newsletter