// NODE.JS ยท STRIPE ยท PAYMENTS ยท HOSTINGER

Stripe Backend: Production-Ready Payments on Hostinger with Node.js

View on GitHub

Integrating Stripe into a Node.js backend sounds straightforward until you hit the details: webhook signature verification, idempotent event handling, correct raw body capture, and deploying to shared hosting without native compilation. This project solves all of it in a minimal, well-documented package targeting Hostinger shared hosting with Node โ‰ฅ 22.

The backend uses Node's built-in node:sqlite module โ€” no npm database driver, no native build step. Every Stripe event is recorded in an append-only audit log. The full test suite runs with Node's built-in test runner. Nothing exotic.

What the backend covers

The project handles the complete Stripe Checkout lifecycle: creating sessions, processing webhook events, persisting orders, and providing read endpoints for inspecting what happened. It's structured for real deployment, not just tutorial code.

01
POST /checkout/create-session โ€” creates a Stripe Checkout Session and pre-inserts a pending order in SQLite. Returns the hosted Stripe checkout URL.
02
POST /webhook โ€” receives signed Stripe events. Raw body is captured before any JSON parsing so the HMAC signature check passes. 9 event types handled.
03
GET /checkout/success โ€” Stripe redirects here after payment. Verifies the session with Stripe and returns the stored order.
04
GET /orders, /orders/:id โ€” read-only inspection endpoints for the SQLite database during development and debugging.
05
GET /orders/events/log โ€” the full Stripe event audit trail, paginated. Every webhook received is stored here with its complete JSON payload.
06
GET /health โ€” returns { "status": "ok" }. Used to verify the Hostinger deployment is alive.

Webhook event handling

One of the trickier parts of a Stripe integration is handling all the events correctly, including retries. The backend processes 9 event types and uses the Stripe event ID as a UNIQUE constraint in SQLite โ€” duplicate deliveries are detected and silently acknowledged without processing twice.

EventDB action
checkout.session.completedCreates order if needed, marks paid
checkout.session.expiredUpdates order status to expired
payment_intent.succeededLogged โ€” order already set by session event
payment_intent.payment_failedLogged + console alert
payment_intent.canceledLogged
charge.succeededLogged
charge.failedLogged + console alert
charge.refundedUpdates order status to refunded
charge.dispute.createdLogged + console warn

Database design

Two tables, each with a clear purpose. orders tracks the lifecycle of each Checkout Session โ€” one row per session, status transitions from pending through paid, expired, or refunded. payment_events is an append-only audit log of every webhook received, including the full JSON payload.

-- orders: one row per Stripe Checkout Session
id              TEXT PK   -- cs_... session id
payment_intent  TEXT      -- pi_... (set after webhook)
customer_email  TEXT
amount_total    INTEGER   -- in cents
currency        TEXT
status          TEXT      -- pending โ†’ paid / expired / refunded
stripe_metadata TEXT      -- JSON blob
created_at      TEXT
updated_at      TEXT

-- payment_events: append-only audit log
id               INTEGER PK AUTOINCREMENT
stripe_event_id  TEXT UNIQUE  -- idempotency key
event_type       TEXT
related_object_id TEXT
payload          TEXT         -- full Stripe JSON
processed_at     TEXT

The stripe_event_id UNIQUE constraint is doing real work here. When Stripe retries a webhook delivery (which it will), the insert fails with a constraint violation, the handler catches it and returns { received: true, duplicate: true }, and no order state is corrupted.

Project structure

stripe-backend/
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ stripe.js           # Stripe client + env validation
โ”œโ”€โ”€ db/
โ”‚   โ””โ”€โ”€ database.js         # Schema, connection, query helpers
โ”œโ”€โ”€ middleware/
โ”‚   โ””โ”€โ”€ rawBody.js          # Raw body capture for webhook HMAC
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ checkout.js         # Create session, /success, /cancel
โ”‚   โ”œโ”€โ”€ orders.js           # Read-only order/event endpoints
โ”‚   โ””โ”€โ”€ webhook.js          # All Stripe event handlers
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ database.test.js    # 27 DB layer unit tests
โ”‚   โ””โ”€โ”€ webhook.test.js     # Webhook handler integration tests
โ”œโ”€โ”€ public/
โ”‚   โ””โ”€โ”€ index.html          # Quick browser test page
โ”œโ”€โ”€ server.js
โ””โ”€โ”€ .env.example

Configuring Stripe keys

Stripe gives you two environments โ€” Sandbox (formerly Test mode) and Live โ€” with completely separate keys and webhook endpoints. The project includes a dedicated STRIPE_KEYS_GUIDE.md that covers every step: finding your API keys in the new Stripe Workbench UI, registering a webhook endpoint and selecting the 9 events, capturing the signing secret, configuring your .env, and setting environment variables in Hostinger's hPanel. It also covers key rotation and the security rules you can't skip.

Three environment variables is all you need to start:

# .env
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_WEBHOOK_SECRET=whsec_...
APP_BASE_URL=https://yourdomain.com
CORS_ORIGINS=https://yourdomain.com
PORT=3000

Never commit .env to Git. The .gitignore already excludes it โ€” verify with git check-ignore -v .env before your first push.

Testing the API

The project ships with two testing tracks: an automated test suite and a curl-based manual guide. The API_TESTING_GUIDE.md walks through every endpoint with exact curl commands, expected responses for both success and error cases, and a full end-to-end payment flow from session creation to webhook confirmation.

Automated tests

The test suite uses Node's built-in node:test runner โ€” no Jest, no Vitest, no extra dependencies. Each test suite gets an isolated :memory: SQLite database, so nothing touches disk and tests are fully repeatable.

npm test

โ–ถ orders table (9 tests)
โ–ถ payment_events table (7 tests)
โ–ถ schema integrity (5 tests)
โ–ถ webhook: checkout.session.completed (2 tests)
โ–ถ webhook: checkout.session.expired (2 tests)
โ–ถ webhook: idempotency (2 tests)
โ–ถ webhook: charge.refunded (1 test)
โ–ถ webhook: event log audit trail (1 test)

tests 34  pass 34  fail 0

Webhook testing with Stripe CLI

The webhook endpoint requires a valid Stripe signature โ€” you can't just curl it directly. Use the Stripe CLI to forward real sandbox events to your local server:

# Terminal 1 โ€” start the server
npm run dev

# Terminal 2 โ€” forward Stripe events to localhost
stripe listen --forward-to localhost:3000/webhook

# Terminal 3 โ€” trigger test events
stripe trigger checkout.session.completed
stripe trigger charge.refunded
stripe trigger charge.dispute.created

Test cards

Card numberScenario
4242 4242 4242 4242Payment succeeds
4000 0000 0000 0002Card declined
4000 0025 0000 31553D Secure required
4000 0000 0000 9995Insufficient funds

Deploying to Hostinger

The README includes a specific Hostinger deployment section because shared hosting has some non-obvious constraints. The key decisions:

Set DB_PATH to an absolute path outside the web root โ€” never inside public_html. The directory is created automatically on first run, but the path must be writable by your Node process. On Hostinger, something like /home/username/stripe-data/stripe.db works reliably.

Use hPanel's Environment Variables section instead of uploading a .env file via SFTP. Add each variable individually, save, and restart the Node.js app. This is safer than managing a secrets file on disk and easier to update without SSH access.

The production checklist in the README covers the full go-live sequence: switching from sandbox to live Stripe keys, registering a live webhook endpoint, locking down the /orders endpoints (no auth in the current implementation), enabling HTTPS, and setting up a SQLite backup schedule.

โ† All Articles