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)
  1. Run server:
npm run dev
  1. Create Stripe tunnel
stripe login
stripe listen --forward-to localhost:8788/webhook/payment
  1. Enter the webhook secret in your .env file

  2. Simulate event and make sure everything returns a 200 status code

stripe trigger payment_intent.succeeded
  1. 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

  2. Use the card 4242 4242 4242 4242 for testing (any CVC and future date)

  3. Check the email inbox for the license key

Susbcribe to the newsletter