Shipped product · Non-AI · Full-stackLive2024

Print ordering platform — shipped, live, and taking payments

Printish

Print ordering platform — customer checkout, order tracking, and an admin dispatch view. Built and launched solo.

Customers upload artwork, pay, and track delivery in one clear flow. Admins manage dispatch, catch stuck jobs early, and keep updates honest when vendor or courier signals go stale. Designed, built, and launched solo.

Live

printish.co.in

2

Customer and admin control surfaces

1

Production failure class fully closed

Solo

End-to-end build, launch, and ownership

Order journey

1

Upload

File validation gate before checkout opens

2

Pay

Hosted Razorpay checkout; secrets stay server-side only

3

Create order

State machine writes the first durable record

4

Fulfil

Admin dispatches to vendor after manual review

5

Deliver

Status stays honest when courier signal is stale

Payment path

Server-side Razorpay integration. Orders stored and tracked via Firebase.

Client/api/create-orderRazorpay
Razorpay/api/verify-paymentsignature check
VerifiedFirestore writeorder record

What broke in production

Webhook retry caused duplicate order updates — inconsistent state.

Signal

Payment looked successful, but the order stayed pending.

Dangerous combination: the buyer sees success while the operational system has not yet secured the order state.

Root cause

Webhook retries raced ahead of a durable order commit.

Duplicate delivery was not hypothetical. The bug sat exactly at the boundary between payment acknowledgement and fulfillment state creation.

Fix

Processing became idempotent and commit ordering was hardened.

Provider event IDs became the guardrail, duplicate deliveries were ignored, and the write path completed before the handler reported success.

Why it matters

Trust in a commerce product is lost at the seams first.

Closing this failure class mattered more than adding another polished screen — it protected both revenue truth and customer confidence.

The fix

Idempotent handlers. Guarded transitions. No corrupted retries.

Idempotent handlers keyed by Razorpay payment event ID

State machine blocks invalid transitions at the write path

Retries silently no-op when the order record already exists

State machine

Explicit transition logic

runtime enforced

Invalid moves throw immediately. No silent corruption on retry.

order_lifecycle.example.ts
typescript
1export function advanceOrder(
2 current: OrderStatus,
3 event: FulfillmentEvent,
4): OrderStatus {
5 const next = transitions[current][event];
6 if (!next) throw new InvalidTransition(current, event);
7 return next;
8}

Guarded state transition: invalid moves throw immediately rather than silently corrupting order state.

Real constraint

Courier updates are unreliable. The system must reflect uncertainty without lying.

Third-party courier signals arrive late, arrive once, or sometimes don't arrive at all. The easy path is to show the last known state as current — which eventually looks wrong to the customer.

The harder path: surface uncertainty explicitly. If the signal is stale, the UI says so. No invented ETAs, no bluffed precision. This erodes trust slower than a confident lie does.

No fake ETA when courier is silent
Stale signals surfaced, not suppressed
Admin flagged when tracking goes cold

What didn't work

Three decisions I'd revisit, and why they happened.

Over-automated vendor assignment

First version assigned vendors without human review. Bad matches surfaced in production — real orders, wrong vendor, real complaints. Replaced with explicit admin dispatch. Slower, far fewer errors.

Two separate print flows

Maintained dual print configuration routes to ship faster. It worked during launch. But any future change now touches two surfaces. The correct call now would be consolidation before adding features.

Large uploads on mobile

File uploads on poor mobile connections have no resumption. The order stalls silently. A proper resumable upload or clear retry affordance is the obvious next thing to fix.

Checkout spine

Four boundaries: browser, Next.js + /api, Razorpay, and Firebase.

Client

Browser · React App

POST /api/*

Server

Next.js + /api route handlers

secrets never leave this layer

Payments

Razorpay

Backend

Firebase

1

Client → /api/create-order → Razorpay (hosted checkout)

2

Razorpay → /api/verify-payment (server-side signature check)

3

Verified → Firestore order write · auth · file storage