# How to Add Order Tracking to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Order tracking needs a Supabase orders table with a status enum and order_status_history audit trail, Supabase Realtime so the tracking page updates without a refresh, a guest lookup flow (order_number + email, no login required), and email or SMS notifications at each status change. With Lovable or V0 you can ship this in 2–4 hours for $0–$30/month at 1,000 orders. The two pieces AI tools consistently miss are notification deduplication and guest enumeration protection.

## What an Order Tracking Feature Actually Is

Order tracking turns a one-way transaction into an ongoing relationship: customers know where their order is without emailing support. The core is a status timeline (Placed → Confirmed → Processing → Shipped → Delivered) that updates in real time, a tracking number that links out to the carrier, and notifications at each step. The feature has two distinct users: the logged-in customer who can see their account history, and the guest who bought without creating an account and needs to look up their order by order number and email. Supabase Realtime removes the need for polling — the tracking page updates the instant an admin or webhook changes the order status. The deceptively tricky parts are notification deduplication (one 'Shipped' email, not three) and preventing order enumeration by guessing sequential order numbers.

## Anatomy of the Feature

Six components build the full order tracking experience. AI tools handle the timeline UI and orders table well; the Realtime subscription reconnection, guest enumeration guard, and notification deduplication require explicit prompting.

- **Order status timeline** (ui): A vertical stepper component rendering six steps: Placed, Confirmed, Processing, Shipped, Out for Delivery, Delivered. The current status is highlighted with a filled circle; completed steps use a checkmark; future steps are greyed out. Built with a shadcn/ui custom stepper or a Framer Motion-animated progress line for visual polish.
- **Orders table** (data): Supabase PostgreSQL table with user_id (nullable for guests), email, order_number (unique, human-readable like 'ORD-A3F8K2'), status with a CHECK constraint on the allowed values, items (jsonb array of product name, quantity, and price), shipping_address (jsonb), tracking_number, carrier, estimated_delivery, and a notified_statuses text[] array that records which status notification emails have already been sent.
- **Status update API** (backend): An internal PATCH /api/orders/[id]/status endpoint (or Server Action) used by admin panels, fulfillment system webhooks, or manual admin updates. Validates the status transition against an allowed transitions map before applying the update. On success, writes a row to order_status_history and triggers the notification Edge Function.
- **Supabase Realtime subscription** (backend): The tracking page subscribes to a Supabase Realtime channel filtered to the specific order_id. When the status column changes — triggered by an admin update or webhook — the UI updates the active step in the timeline without a page reload. The subscription is set up as a client component using the Supabase JS SDK.
- **Guest tracking page** (ui): A public /track page with a form accepting order_number and email. On submit, a Server Action or Edge Function queries orders WHERE order_number = $1 AND lower(email) = lower($2) — the email match prevents enumeration of sequential order numbers. Returns the same timeline and order summary as the logged-in view, but without auth.
- **Notification handler** (backend): A Supabase Edge Function triggered on each status update sends a templated email via Resend (e.g., 'Your order ORD-A3F8K2 has shipped — track it here: [link]') and optionally an SMS via Twilio if a phone number is on file. Before sending, checks notified_statuses to prevent duplicate notifications. After sending, appends the status to notified_statuses using an atomic array_append UPDATE.

## Data model

Two tables — orders and order_status_history — give you full tracking with an audit trail. Paste into the Supabase SQL editor and run before prompting your AI tool.

```sql
create table public.orders (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete set null,  -- nullable for guest orders
  order_number text not null unique default 'ORD-' || upper(substring(gen_random_uuid()::text, 1, 8)),
  email text not null,
  status text not null default 'placed' check (
    status in ('placed','confirmed','processing','shipped','out_for_delivery','delivered','cancelled')
  ),
  items jsonb not null default '[]',          -- [{name, qty, price_cents}]
  subtotal_cents int not null default 0,
  shipping_cents int not null default 0,
  total_cents int not null default 0,
  currency text not null default 'usd',
  shipping_address jsonb,
  tracking_number text,
  carrier text,
  estimated_delivery date,
  cancellation_reason text,
  notified_statuses text[] not null default '{}',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

alter table public.orders enable row level security;

-- Logged-in users see their own orders
create policy "Users view own orders"
  on public.orders for select
  using (auth.uid() = user_id);

-- Guest tracking: handled in Edge Function / Server Action — no RLS policy needed
-- because guest lookup uses service role key server-side, validates email match in code

-- Admin update (service role bypasses RLS — call from Edge Function / Server Action)

-- Status history for audit trail
create table public.order_status_history (
  id uuid primary key default gen_random_uuid(),
  order_id uuid references public.orders(id) on delete cascade not null,
  status text not null,
  changed_at timestamptz not null default now(),
  changed_by text  -- 'system', 'admin:{user_id}', 'webhook:shipstation'
);

alter table public.order_status_history enable row level security;

create policy "Users view own order history"
  on public.order_status_history for select
  using (
    order_id in (
      select id from public.orders where user_id = auth.uid()
    )
  );

create index orders_user_idx on public.orders (user_id, created_at desc);
create index orders_guest_lookup_idx on public.orders (order_number, lower(email));
create index order_history_order_idx on public.order_status_history (order_id, changed_at);

-- Auto-update updated_at
create or replace function update_updated_at()
returns trigger language plpgsql as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create trigger orders_updated_at
  before update on public.orders
  for each row execute function update_updated_at();
```

The orders_guest_lookup_idx index on (order_number, lower(email)) makes guest lookups fast while enforcing the two-field match that prevents sequential number enumeration. The notified_statuses text[] column is the deduplication guard — check it before every notification send.

## Build paths

### Lovable — fit 5/10, 2–4 hours

Best fit — Lovable generates Supabase schema with Realtime, the tracking timeline component, guest lookup page, and Resend email notification Edge Function in one session.

1. Create a new Lovable project and connect Lovable Cloud so Supabase auth and database are provisioned
2. Paste the prompt below; Agent Mode builds the order tracking page, guest /track page, status update API, and notification Edge Function
3. In Lovable Cloud > Secrets, add RESEND_API_KEY and optionally TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER
4. Publish to live URL and test the Realtime subscription by opening the tracking page, then updating an order status in the Supabase Table Editor — the timeline should update within 1 second

Starter prompt:

```
Build an order tracking feature. Orders table: user_id (nullable for guests), email, order_number (unique, format 'ORD-' + 8 random uppercase chars), status enum (placed, confirmed, processing, shipped, out_for_delivery, delivered, cancelled), items jsonb array (name, qty, price_cents), total_cents, shipping_address jsonb, tracking_number, carrier, estimated_delivery date, notified_statuses text[] default '{}', created_at, updated_at. Also create order_status_history(id, order_id, status, changed_at, changed_by). Order tracking page at /orders/[id]: vertical stepper timeline with 6 steps (Placed, Confirmed, Processing, Shipped, Out for Delivery, Delivered) — current step highlighted, cancelled state with red indicator and reason. Show tracking number with carrier link (build a carrier URL map: UPS, FedEx, USPS, DHL, fallback Google search). Show estimated delivery date. Show items list with name, qty, and price. Subscribe to Supabase Realtime on the orders channel filtered by order_id — update stepper on status change without page refresh. Add visibilitychange listener that re-subscribes when tab becomes visible after sleep; show 'Reconnecting...' text during re-subscription. Guest tracking page at /track: form with order_number and email fields; Server Action or Edge Function queries orders WHERE order_number = $1 AND lower(email) = lower($2); rate-limit to 5 attempts per IP per hour; return generic error 'Order not found' without revealing which field was wrong. Status update API: PATCH /api/orders/[id]/status; validate transition against allowed map (placed→confirmed|cancelled, confirmed→processing, processing→shipped, shipped→out_for_delivery, out_for_delivery→delivered); insert row to order_status_history; trigger notification Edge Function. Notification Edge Function: before sending check if status already in notified_statuses array; send email via Resend with order number, status name, tracking link; after sending, UPDATE orders SET notified_statuses = array_append(notified_statuses, $status). Include 'Contact Support' CTA on tracking page. RLS: logged-in users see only own orders. Guest lookup uses service role key server-side.
```

Limitations:

- Supabase Realtime in the Lovable in-editor preview may lag or disconnect — test real-time updates on the published URL
- SMS via Twilio requires manual Secrets setup in Lovable Cloud after generation; the Edge Function body is scaffolded but the credentials must be added manually
- Status transition validation may need review — confirm the allowed transitions map matches your business logic before using in production

### V0 — fit 4/10, 3–5 hours

Excellent timeline UI with smooth step animations; Next.js Server Actions handle status updates cleanly and Vercel deployment enables webhook integration with fulfillment platforms.

1. Paste the prompt below; V0 generates the OrderTimeline client component, TrackingPage server component, guest /track page, and Server Actions
2. Add environment variables in the Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY
3. Run the SQL schema from this page in the Supabase SQL editor
4. Deploy to Vercel; test the Realtime subscription from two browser tabs — update order status in the Supabase Table Editor and watch the tracking page update live

Starter prompt:

```
Build an order tracking feature in Next.js 14 App Router using Supabase and Resend. OrderTimeline client component: receives current status string and renders a vertical stepper with 6 steps (Placed, Confirmed, Processing, Shipped, Out for Delivery, Delivered); current step highlighted with filled circle and label; completed steps show checkmark; future steps greyed out; cancelled state with red indicator and cancellation_reason text. Uses Supabase Realtime to subscribe to orders channel filtered by order_id; updates stepper on status change. Include useEffect with document.addEventListener('visibilitychange') that calls channel.subscribe() when document becomes visible. Tracking page at /orders/[id]: server component fetching order by id for auth.uid(); renders OrderTimeline, tracking number with carrier link (carrier URL map: UPS/FedEx/USPS/DHL/fallback), estimated delivery date, items list (name, qty, price). Guest page at /track: form with order_number and email; Server Action queries orders WHERE order_number = $1 AND lower(email) = lower($2) using service role key; rate-limit via IP check (store attempt counts in Supabase); return generic error on no match. Status update Server Action: validate transition map; update orders.status; insert order_status_history row; call sendNotification(). sendNotification Server Action: check orders.notified_statuses does not include new status; send Resend email with order number and tracking link; UPDATE SET notified_statuses = array_append(notified_statuses, status) atomically. Carrier URL map: UPS → 'https://www.ups.com/track?tracknum=', FedEx → 'https://www.fedex.com/apps/fedextrack/?trknbr=', USPS → 'https://tools.usps.com/go/TrackConfirmAction?tLabels=', DHL → 'https://www.dhl.com/en/express/tracking.html?AWB=', unknown → 'https://www.google.com/search?q=track+'. Include Contact Support CTA linking to /support on tracking page.
```

Limitations:

- Supabase Realtime subscription in Next.js App Router must be a client component — the OrderTimeline component must be marked 'use client' and server props passed to it as initial state
- No auto-provisioning; Supabase schema, Resend API key, and service role key must all be wired manually
- SSR for the guest tracking page needs careful handling — avoid reading from cookies that are not set during the server render to prevent hydration mismatch

### Custom — fit 3/10, 3–5 days

Worth it when tracking must integrate deeply with carrier APIs for automatic status updates — ShipStation, EasyPost, or Shippo webhooks push real tracking events directly into your orders table without manual admin updates.

1. Register webhooks with EasyPost or Shippo using the deployed Vercel URL as the callback — these push tracking events (out_for_delivery, delivered) directly to your API route without manual admin updates
2. Map carrier tracking event codes to your status enum (EasyPost 'out_for_delivery' → 'out_for_delivery', Shippo 'TRANSIT' → 'shipped') in a carrier adapter layer
3. Add multi-package support: a shipments table (order_id, tracking_number, carrier, package_index) so orders with multiple packages each have a separate timeline
4. Build a white-label tracking page with custom domain (track.yourbrand.com) and logo injection via a theme table keyed by seller_id for B2B2C marketplaces

Limitations:

- EasyPost and Shippo add per-shipment costs ($0.05–$0.10/shipment) on top of carrier rates — factor into pricing before committing to deep carrier API integration
- Carrier webhook formats vary significantly — EasyPost and Shippo normalize them, but direct carrier API integrations (UPS, FedEx API) each have their own auth and schema

## Gotchas

- **Duplicate 'Shipped' notification emails** — Stripe, fulfillment webhooks, and retry logic all fire events multiple times. If the notification Edge Function doesn't check notified_statuses before sending, a customer receives two or three 'Your order has shipped' emails within minutes — a support ticket waiting to happen. Fix: Before sending any notification, query orders.notified_statuses and check if the status is already in the array. If present, exit early. After a successful send, execute UPDATE orders SET notified_statuses = array_append(notified_statuses, $1) WHERE id = $2 in the same database transaction. As a second layer, pass a Resend idempotency key combining order_id and status — Resend will silently discard duplicate sends with the same key.
- **Realtime subscription disconnects after tab sleep** — Chrome and Safari aggressively suspend background tabs after approximately 5 minutes of inactivity. When the user returns to the tracking page, the Supabase Realtime WebSocket channel has disconnected. The status may have changed while the tab was asleep, and the page shows stale data with no indication that updates were missed. Fix: Add document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { channel.subscribe(); } }) in the client component. Simultaneously fetch the current order status via a regular query on visibility change to catch any updates that arrived while disconnected. Show a 'Reconnecting...' text during the re-subscription window.
- **Guest tracking allows order number enumeration** — AI tools generate the guest /track endpoint with only order_number validation. Sequential order numbers (ORD-0001, ORD-0002) can be iterated automatically — anyone can scrape all order details, emails, shipping addresses, and items from your entire order database without authentication. Fix: Require BOTH order_number AND email match server-side before returning any data. Use a random order number format ('ORD-' + 8 random alphanumeric characters) rather than sequential integers. Add IP-based rate limiting (5 attempts per hour) and return a generic 'Order not found' message that does not confirm whether the order number or email was wrong.
- **Invalid status transitions allowed** — AI generates a status update API that accepts any status value and sets it directly, with no transition validation. An admin or webhook can set status to 'delivered' from 'placed', skipping 'shipped' entirely. This breaks any business logic that depends on sequential transitions (triggering packing notifications, for example) and creates audit history gaps. Fix: Define an allowed transitions map in the status update Server Action: { placed: ['confirmed', 'cancelled'], confirmed: ['processing'], processing: ['shipped'], shipped: ['out_for_delivery'], out_for_delivery: ['delivered'] }. Reject transitions not in the map with a 422 error and a clear message: 'Cannot transition from processing to delivered — must ship first.'
- **Tracking link broken for unknown or domestic-only carriers** — AI tools often generate a naive tracking URL by concatenating a carrier domain string with the tracking number (carrier + '.com/track?=' + trackingNumber). This works for UPS and FedEx but breaks for USPS (uses a different query parameter), DHL, regional carriers, and any carrier with a non-standard URL structure. Fix: Build a carrier URL map: UPS → 'https://www.ups.com/track?tracknum=', FedEx → 'https://www.fedex.com/apps/fedextrack/?trknbr=', USPS → 'https://tools.usps.com/go/TrackConfirmAction?tLabels=', DHL → 'https://www.dhl.com/en/express/tracking.html?AWB='. For unknown carriers, fall back to 'https://www.google.com/search?q=track+' + encodeURIComponent(carrier + ' ' + trackingNumber). State the carrier URL map explicitly in your prompt.

## Best practices

- Deduplicate notification emails at two layers: the notified_statuses array in your database and a Resend idempotency key — webhooks and retries will trigger your notification handler more than once
- Enforce status transition validation in the API layer with a defined transitions map — do not rely on admin UI constraints alone, as webhooks from fulfillment systems bypass the UI entirely
- Add a visibilitychange re-subscription listener to your Realtime client — without it, users returning to a sleeping tab see stale status and think tracking is broken
- Always require both order_number and email for guest lookups and use a random order number format — sequential numeric IDs combined with single-field lookup are an enumeration vulnerability
- Store carrier tracking links in a URL map rather than string concatenation — at least four major carriers have non-standard URL query parameter names
- Use the notified_statuses text[] array rather than a separate notifications table for simple deduplication — one array column is simpler to query and update atomically
- Show an estimated delivery date prominently at the top of the tracking page, not just the current status — ETA is the primary information most customers are seeking

## Frequently asked questions

### How do I show real-time tracking updates without the user refreshing?

Use Supabase Realtime. On the tracking page, subscribe to the orders table filtered by the specific order_id using the Supabase JS SDK channel API. When your status update API or webhook changes the order status, Supabase pushes the event to all connected clients instantly. The tracking page timeline updates without any user action. Add a visibilitychange listener so the subscription re-establishes when the user returns to a sleeping browser tab.

### Can guests track orders without creating an account?

Yes. Build a public /track page with a form accepting order_number and email. Your Server Action or Edge Function queries orders WHERE order_number = $1 AND lower(email) = lower($2) using the Supabase service role key (server-side only). Return the same tracking timeline and order summary as the logged-in view. Rate-limit to 5 attempts per IP per hour and return a generic 'Order not found' error without confirming which field was wrong.

### How do I send SMS tracking notifications?

Add Twilio to your notification Edge Function. When an order status changes, check if the order has a phone number on file and if the customer opted into SMS. Call the Twilio Messages API with a short template: 'Your order ORD-A3F8K2 has shipped. Track it: [link]'. At approximately $0.0079/SMS (US), SMS for 1,000 shipped orders costs about $8. Include phone number as an opt-in field at checkout rather than requiring it.

### What carriers can I integrate with for automatic status updates?

EasyPost and Shippo are the two services worth integrating for automatic tracking — both normalize carrier events from UPS, FedEx, USPS, DHL, and dozens of regional carriers into a consistent webhook format. When a carrier scans a package, EasyPost/Shippo sends a webhook to your PATCH /api/orders/[id]/status endpoint, updating the status automatically without admin intervention. Pricing is $0.05–$0.10 per shipment.

### How do I handle orders with multiple packages?

Add a shipments table: (id, order_id, tracking_number, carrier, status, estimated_delivery). The tracking page renders one timeline per shipment rather than one per order. The order-level status advances to 'delivered' only when all shipments in that order reach delivered status — use a database trigger or Server Action that checks COUNT(*) WHERE status != 'delivered' = 0. Explicitly prompt for this table if multi-package orders are a day-one requirement.

### Can I customize the tracking page with my branding?

Yes — the tracking page is a standard Next.js or Lovable page under your full control. Add your logo, brand colors, and custom copy. For white-label tracking (serving other sellers' customers under their domain), you need a custom domain setup (e.g., track.sellerbrand.com as a CNAME to your Vercel deployment) and a themes table keyed by seller_id that injects logo URL and primary color.

### How do I notify customers at each status change?

Trigger a notification Edge Function from your status update API route. The function checks orders.notified_statuses to see if this status has already triggered an email; if not, it sends via Resend using a status-specific template (shipped template includes carrier and tracking link; out_for_delivery template says 'arriving today'), then appends the status to notified_statuses atomically. This ensures each status triggers exactly one email per customer.

### What should I do when a shipment is delayed?

Update the estimated_delivery date on the orders row and send a proactive 'delay notification' email — a separate notification type outside the normal status flow. Store a delay_reason text field on the orders table and display it below the timeline with copy like 'Weather delay — new estimated delivery: Jan 15.' Proactive delay communication reduces support volume more than any other single change to the order tracking feature.

---

Source: https://www.rapidevelopers.com/app-features/order-tracking
© RapidDev — https://www.rapidevelopers.com/app-features/order-tracking
