YC
PRODUCTION APPLICATION · PUBLIC WEB GATEWAY & SERVICE

TrackMyPost · Postal Tracking Gateway & Polling Engine

A public postal tracking web application and API gateway live at trackmypost.in. Built with a Rust/Yew WebAssembly frontend and an asynchronous FastAPI backend gateway implementing barcode format validation, two-phase adaptive polling, and dynamic server-side OpenGraph card injection for shareable proof-of-delivery receipts.

LIVE SERVICEtrackmypost.in↗Public Web Utility
FRONTEND ENGINERust + Yew (WASM)Trunk Bundled Client
POLLING WORKFLOWTwo-Phase Adaptive15s / 4s up to 95s
DEPLOYMENTDocker Compose + NginxPrometheus Observability
TrackMyPost public homepage

Figure 1: Public tracking interface live at trackmypost.in with single consignment input, real-time status pill, transit timeline, and bulk inquiry flow.

02. THE OPERATIONAL PROBLEM

Brittle Postal Lookups & The Latency of Asynchronous Data Retrieval

In Indian legal proceedings and institutional arbitration, Speed Post through India Post is widely utilized for serving notices and summonses. Arbitrators and legal practitioners require verifiable delivery records (such as delivery date, recipient postal circle, and status timestamp) to confirm service before proceeding with scheduled hearings.

However, retrieving delivery status through official channels presented major engineering and usability challenges:

  • Unpredictable Retrieval Latency: Cold tracking requests against government systems can take anywhere from 10 to 60+ seconds to resolve while background scrapers navigate session cookies and CAPTCHAs.
  • No Shareable Tracking Previews: Official portals do not provide direct, shareable URLs to review tracking progress with clients, colleagues, or litigants on messaging platforms like WhatsApp or Slack.
  • Fragile Client Connections: If a web client opens a standard synchronous HTTP connection and waits 45 seconds for a response, cellular mobile connections frequently time out, drop TCP sockets, or leave users wondering if the request failed.
03. WHAT I BUILT & SYSTEM BOUNDARIES

Tracking Gateway, WASM Client & Upstream Separation

I built the public-facing TrackMyPost application, WebAssembly frontend, and FastAPI gateway from scratch, establishing a clear boundary with the internal scraping infrastructure:

WHAT I PERSONALLY BUILT
  • Rust/Yew WebAssembly frontend client compiled with Trunk.
  • FastAPI gateway service mediating public client requests.
  • Consignment barcode validation using verified regex patterns.
  • Two-phase adaptive polling loop with timeout guards.
  • Dynamic server-side OpenGraph card generation for /y/{tracking}.
  • Client-side recent searches saved in browser localStorage.
  • Docker multi-container stack, Nginx reverse proxy, and Prometheus metrics.
UPSTREAM SYSTEM BOUNDARIES
  • Upstream Scraping Engine: The automated postal scraping and CAPTCHA-solving pipeline is a separate internal service. I did not author that scraper; my gateway consumes its internal endpoints.
  • Bulk Referral Workflow: The web platform handles single tracking lookup; high-volume bulk inquiries are routed via a dedicated WhatsApp referral channel.
  • Proof-of-Delivery PDF Generation: PDF document generation is handled upstream and proxied through the gateway when enabled.
04. ARCHITECTURE & REQUEST FLOW

End-to-End Tracking Lifecycle

When a user tracks a consignment, the request travels through Nginx to the FastAPI gateway, which orchestrates cache verification, upstream task scheduling, and adaptive status polling:

TRACKING REQUEST & POLLING LIFECYCLE:
[ Browser Client / Messaging Crawler ]
                     │
                     ▼ (HTTPS Request)
┌────────────────────────────────────────────────────────────────────────┐
│ NGINX Edge Reverse Proxy                                               │
│ • Serves compiled Rust/Yew WASM assets (types { application/wasm; })   │
│ • Reverse proxies /api/* and /y/* routes to internal Uvicorn ASGI port │
└────────────────────────────────────────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────────────────┐
│ FastAPI Gateway Service (main.py)                                      │
│                                                                        │
│ Case A: Social Crawler Hits /y/{tracking}                             │
│   • Reads dist/index.html shell                                        │
│   • Injects dynamic <meta property="og:title" ...>                   │
│   • Returns hydrated HTML immediately without waiting for browser JS   │
│                                                                        │
│ Case B: Client Calls GET /api/method/track/{tracking}                 │
│   1. Validates barcode: ^[A-Za-z]{2}[0-9]{9}IN$                        │
│   2. Checks upstream record (get_doc):                                 │
│      • If status == "file downloaded" → Returns immediately            │
│      • If record missing → Calls insert_doc to enqueue scrape          │
│   3. Executes poll_until_ready():                                      │
│      • Phase 1 (0–48s): Polls upstream every 15 seconds                │
│      • Phase 2 (48–95s): Polls upstream every 4 seconds                │
│      • If elapsed >= 95s: Emits timeout log and returns failure        │
└────────────────────────────────────────────────────────────────────────┘
                     │
                     ▼ (Internal HTTP Requests)
┌────────────────────────────────────────────────────────────────────────┐
│ Separate Internal Postal Scraping Service                              │
│ • Solves postal CAPTCHAs, scrapes India Post tracking records          │
│ • Updates document status: "start trac" → "processing" → "file downloaded"
└────────────────────────────────────────────────────────────────────────┘
05. ENGINEERING DEEP DIVES

Implementation Decisions Backed by Source Code

1. Two-Phase Adaptive Polling Loop with Hard Timeout Protection

Cold tracking requests from India Post rarely resolve in under 10 seconds due to session handshakes and CAPTCHA processing. Polling every second would flood the internal service with redundant queries; conversely, polling every 15 seconds throughout the entire lifecycle would delay delivering completed results.

In backend/main.py, I implemented a two-phase adaptive poll strategy: polling every 15 seconds during the initial 48-second phase, then accelerating to 4-second intervals as the request nears expected completion, bounded by a 95-second hard ceiling:

Sanitized Source Excerpt: backend/main.py
MAX_WAIT_SECONDS = 95
PHASE1_UNTIL     = 48
PHASE1_INTERVAL  = 15
PHASE2_INTERVAL  = 4

WAITING_STATUSES = {
    "start trac", "start_trac", "processing", "queued",
    "file added", "redo", "reprocess", "manual check", "error"
}
BASIC_READY_STATUSES = {"file downloaded"}

async def poll_until_ready(tracking: str, allow_basic_ready: bool = False) -> dict | None:
    start_time = time.time()

    while True:
        elapsed = time.time() - start_time
        if elapsed >= MAX_WAIT_SECONDS:
            logger.info("poll_timeout", extra={"request_id": get_request_id(), "tracking": tracking})
            return None

        doc = get_doc(tracking)
        if doc:
            status_raw = (doc.get("status") or "").strip().lower()
            if allow_basic_ready and status_raw in BASIC_READY_STATUSES:
                return normalize(doc)
            if status_raw not in WAITING_STATUSES:
                return normalize(doc)

        # Adaptive sleep interval based on elapsed time
        sleep_for = PHASE1_INTERVAL if elapsed < PHASE1_UNTIL else PHASE2_INTERVAL
        await asyncio.sleep(sleep_for)

2. Server-Side OpenGraph Tag Injection on a Client-Rendered WASM SPA

Because the frontend is a client-side rendered (CSR) Rust WebAssembly Single Page Application, messaging crawlers (WhatsApp, Twitter, Slack, iMessage) that do not execute WebAssembly would see blank social preview cards when users shared tracking links.

Instead of maintaining a heavy Node.js Server-Side Rendering (SSR) cluster, I engineered a lightweight server-side template injector in FastAPI on the /y/{tracking} route. The gateway reads the static dist/index.html template into memory, checks tracking status, replaces OpenGraph placeholders dynamically, and returns the modified HTML string via fast in-memory string substitution without invoking an external rendering runtime:

Sanitized Source Excerpt: backend/main.py
@app.get("/y/{tracking}", response_class=HTMLResponse)
async def share_tracking(tracking: str):
    tracking = tracking.strip().upper()
    if not is_valid_tracking(tracking):
        raise HTTPException(400, "Invalid tracking number format")

    doc = get_doc(tracking)
    status_text = "Tracking in progress"
    if doc:
        status_text = doc.get("status") or status_text

    # Read base WebAssembly HTML shell
    html_content = read_dist_index_html()
    
    # Inject dynamic OpenGraph meta tags for rich social preview unfurling
    og_title = f"India Post Tracking: {tracking}"
    og_desc  = f"Current Status: {status_text}. Track verified delivery proof on TrackMyPost."
    
    html_content = re.sub(r'<meta property="og:title" content="[^"]*"', f'<meta property="og:title" content="{og_title}"', html_content)
    html_content = re.sub(r'<meta property="og:description" content="[^"]*"', f'<meta property="og:description" content="{og_desc}"', html_content)
    
    return HTMLResponse(content=html_content)

3. Client-Side Rust/Yew Architecture & Barcode Validation

The web client is built using Rust with the Yew component framework. Before firing any network request, the client enforces the India Post 13-character consignment barcode specification:

^[A-Za-z]{2}[0-9]{9}IN$

Consignments must begin with two alphabet characters (e.g., “ED” for Speed Post, “EM” for Express Mail), followed by 9 numeric digits, and terminate strictly with “IN” (India country identifier). Invalid formats trigger immediate client-side validation feedback, preventing unnecessary upstream API queries. Successfully queried tracking numbers are cached in browser localStorage so users can revisit active consignments in one click.

06. ENGINEERING CHALLENGES & TRADE-OFFS

Real Technical Constraints & Architectural Mitigations

HANDLING VARIABLE ASYNCHRONOUS DATA LATENCY

Technical Constraint: Unlike standard transactional databases that respond in milliseconds, upstream postal lookups introduce variable resolution delays. Browsers opening unmanaged long-lived synchronous connections risk socket resets or gateway timeouts.

Implementation: Bounded the waiting loop with two-phase adaptive polling intervals (1.5s initial, 3.0s subsequent) and an explicit 95-second ceiling. Terminal statuses (“file downloaded”) return immediately on detection without waiting for the full polling duration.

WASM MIME TYPE MISMATCH IN NGINX

Difficulty: Default Linux Nginx configurations often serve .wasm files as application/octet-stream instead of application/wasm, causing modern browsers to reject WebAssembly.instantiateStreaming and forcing slower fallback compilation.

Mitigation: Configured explicit MIME declarations in nginx.conf: types { application/wasm wasm; }, allowing browsers to stream and compile the WebAssembly binary concurrently with network download.

07. DEPLOYMENT & OPERATIONS

Production Topology & Monitoring

TrackMyPost is containerized using Docker Compose and served behind an Nginx reverse proxy on Linux:

NGINX REVERSE PROXY

Terminates TLS, compresses assets with gzip, serves static WASM bundles, and proxies /api/* to Uvicorn.

PROMETHEUS METRICS

Exposes tracking request counts, polling durations, and upstream error rates on /metrics.

STRUCTURED LOGGING

ASGI middleware generates unique request correlation IDs for every tracking query, logging JSON records to stdout.

08. AUTHENTIC TECHNICAL LIMITATIONS & LESSONS

Identified Technical Debt & Next Steps

A rigorous engineering review of the current implementation highlights two authentic architectural trade-offs:

  • Synchronous HTTP Calls in Async Coroutines: In the current backend/main.py implementation, get_doc() utilizes Python's synchronous requests library rather than an asynchronous client like httpx.AsyncClient. While wrapped inside an async handler, synchronous socket operations can block the worker event loop under high concurrent loads. The next iteration should migrate all upstream calls to non-blocking httpx.AsyncClient with connection pooling.
  • Long-Polling Coroutine Occupancy: Holding an HTTP connection open for up to 95 seconds occupies server coroutine state. A more scalable long-term architecture would transition status updates to Server-Sent Events (SSE) or WebSockets, freeing server threads while background workers push status updates.
09. VERIFIED EVIDENCE & LIVE DEMO
PUBLIC ACCESS:

TrackMyPost is a live, publicly accessible utility. You can test live tracking with any valid 13-character Speed Post consignment number directly at trackmypost.in.