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