DEV Community

Cover image for Building an eSIM Platform With AI: My Technical Rebuild
Foued L
Foued L

Posted on

Building an eSIM Platform With AI: My Technical Rebuild

I rebuilt SafarieSIM—a travel eSIM platform—from a WordPress store into a production system that provisions connectivity automatically. The challenge wasn't just migrating data or swapping frameworks. It was replacing manual order processing with real-time API integration, preventing duplicate provisioning, and building an architecture that could scale without breaking.

Laptop with backend code beside a smartphone prepared for eSIM activation

The rebuild connected an eSIM storefront to automated provisioning and customer delivery.

Smartphone and laptop representing an automated eSIM platform infrastructure.

Rebuilding an eSIM store into an automated provisioning platform required a production-grade backend.

This is a technical case study of what worked, what AI tools got wrong, and where human judgment was still required. I used Claude for architecture decisions and refactoring, ChatGPT for debugging webhook flows, and Lovable for rapid UI prototyping. The stack landed on Next.js, Supabase with PostgreSQL, Stripe for payments, and direct eSIM provider APIs for provisioning.

The hardest problems weren't the ones I expected. Race conditions in webhook handling caused duplicate eSIM assignments. Row-level security in Supabase required rethinking data access patterns. WordPress migration scripts needed multiple passes to preserve SEO without breaking URLs. AI helped me move faster, but it didn't replace the need to understand distributed systems, idempotency, or database constraints.

Why I replaced WordPress and WooCommerce with a purpose-built platform

SafarieSIM started on WordPress because it was fast to launch. WooCommerce handled payments, ACF managed product variants, and a plugin hit the eSIM provider API after checkout. It worked for the first 50 customers, then the cracks showed.

The core problem: WordPress treats eSIMs like physical products. When a customer bought a data plan, WooCommerce fired a webhook, my plugin called the provider API, then stored the eSIM credentials in post meta. But webhooks aren't guaranteed to fire once. I started seeing duplicate provisioning—same order, two eSIMs allocated, one wasted. The provider charged per eSIM whether it was used or not.

I added a transient lock, then tried order status checks, but race conditions persisted under load. WooCommerce's order flow wasn't built for inventory you provision in real-time from a third-party API. Managing customer support meant digging through wp_postmeta tables and cross-referencing API logs in a separate system.

I needed atomic provisioning, proper idempotency, and a data model where eSIMs, orders, and users lived in actual relational tables—not serialized WordPress metadata.

The production stack: Next.js, Supabase, Stripe, and eSIM provider APIs

I landed on Next.js 14 with the App Router, Supabase for PostgreSQL and auth, Stripe for payments, and direct integration with eSIM provider APIs. The old WordPress site had no real concept of async provisioning—orders went into WooCommerce, then someone manually triggered the eSIM purchase. I needed automated provisioning that could survive webhook retries and race conditions.

The first version I built with Claude had a critical bug: Stripe would retry failed webhooks, and each retry created a new eSIM order in the database. Customers ended up with duplicate provisioning attempts. I fixed it by adding a unique constraint on stripe_payment_intent_id in the orders table and wrapping the insert in a transaction. If the webhook fired twice, the second attempt would fail silently at the database level—idempotency through constraints, not application logic.

AI was helpful for scaffolding the Supabase Row Level Security policies, but I had to rewrite most of them. ChatGPT suggested putting the eSIM provider API key in an environment variable accessible to the client, which would have leaked credentials to the browser. I moved all provider calls into Next.js Server Actions and used Supabase service role keys only on the server. The SafarieSIM platform now provisions eSIMs within seconds of payment confirmation, with provider credentials never exposed to the client.

Migrating WordPress customers, orders, products, and URLs without corrupting data

The WordPress database held 2,400 customer records, 1,800 orders, and 60 products spread across WooCommerce's wp_posts, wp_postmeta, wp_users, and wp_usermeta tables. Moving that into PostgreSQL while preserving customer login credentials and order history required surgical precision.

I used Claude to write a Python migration script that extracted WooCommerce's entity-attribute-value structure and normalized it into proper relational tables. The script handled WordPress's password hashing by storing bcrypt hashes in a legacy_password_hash column, then triggering a password reset email on first login to migrate users to Supabase Auth.

Product URLs were the trickiest part. WordPress used /product/thailand-esim/ while I wanted /esim/thailand for better SEO. I generated a redirect map in the migration script and implemented it using Next.js middleware, checking each incoming path against a redirects table in Supabase before serving the page. Google Search Console confirmed zero 404 errors after the cutover.

AI suggested storing order metadata as JSON, which I rejected. Instead, I created proper foreign keys between orders, customers, and esim_activations tables, making queries simpler and enabling referential integrity. The migration at SafarieSIM took 18 seconds to run and required manual verification of 50 sample records before going live.

Designing automated eSIM provisioning from checkout to customer delivery

The hardest part of building an eSIM platform is getting the provisioning pipeline right. When a customer buys data through Stripe, the system needs to request an eSIM from the provider API, store the credentials, and email the QR code—all without duplicating orders or exposing sensitive data.

I used AI to prototype the Stripe webhook handler and eSIM API client, but the first version had a critical flaw: duplicate webhooks triggered multiple provisioning requests for the same order. Claude suggested adding a unique constraint on order_id in PostgreSQL, which solved the race condition by making duplicate inserts fail atomically.

The full flow now runs like this: Stripe webhook hits the Next.js API route, the handler checks if the order exists, provisions the eSIM via the provider's REST API, stores the ICCID and activation code in Supabase, and triggers a transactional email with the QR code. Row Level Security policies ensure customers only see their own eSIM records.

Smartphone receiving a digital mobile plan beside a laptop showing server activity

The production flow links payment confirmation, provider provisioning, and eSIM delivery.

I wrote about the initial architecture decisions in the original case study, but the provisioning logic required the most manual debugging and testing to get production-ready.

Preventing duplicate eSIM provisioning with idempotency and database constraints

The scariest bug I hit was duplicate eSIM provisioning. Stripe sends multiple webhook events for a single payment, and if two checkout.session.completed webhooks arrive within milliseconds, you can provision two eSIMs and bill your provider twice for the same order.

My first attempt used application-level flags—checking if an order already had an eSIM before calling the provider API. AI suggested this pattern, and it looked fine in isolation. But under load, two webhook handlers could both read "no eSIM yet" before either wrote one back. Classic race condition.

The fix was moving idempotency into the database. I added a unique constraint on stripe_session_id in the esim_orders table. Now the second webhook insert fails at the database level with a constraint violation, which I catch and log as a no-op. PostgreSQL enforces atomicity; the application doesn't need to.

I also store Stripe's idempotency_key for any refund or charge API call, so retries don't double-charge customers. This is one area where AI-generated code often skips production-critical details—idempotency isn't glamorous, but it's the difference between a demo and a platform handling real money and connectivity.

Securing customer data with Supabase Row Level Security and server-only credentials

Once customers could provision eSIMs automatically, I needed to lock down who could see what data. eSIM orders contain QR codes that grant mobile connectivity—leak one and someone gets free data on your dime.

Supabase Row Level Security let me enforce data isolation at the database layer. I wrote policies so users could only query their own orders:

CREATE POLICY "Users see only their orders" ON orders FOR SELECT USING (auth.uid() = user_id);

This meant even if my frontend code had a bug, the database would reject unauthorized queries. I used Claude to draft the initial RLS policies, then tested edge cases manually—shared accounts, admin access, webhook writes—because the AI-generated examples assumed single-user patterns and missed service role authentication.

For eSIM provider API keys, I stored them in Supabase secrets and called the provider APIs exclusively from Next.js Server Actions. The credentials never touched the browser. Early on I accidentally embedded a test API key in a client component; ChatGPT caught it during a code review prompt, but I added a pre-commit hook to scan for common secret patterns as a safety net.

RLS and server-only secrets became the foundation for secure multi-user eSIM management without building a custom auth system from scratch.

Handling Stripe webhooks, retries, failures, refunds, and order state transitions

Stripe webhooks are critical for production but also a common source of bugs. The payment platform sends event notifications asynchronously, and your system needs to handle retries, duplicates, failures, and out-of-order delivery.

I built the webhook handler to be idempotent by design. Every incoming event is logged to a stripe_events table with a unique constraint on event_id. If Stripe retries the same event, the database rejects the duplicate and the handler returns 200 immediately—no duplicate provisioning, no duplicate emails.

Order state transitions were harder. A payment_intent.succeeded event should provision an eSIM, but what if the provisioning API fails? I added a provisioning_status column with states like pending, provisioning, completed, and failed. A background cron job retries failed orders every 15 minutes until they succeed or hit a manual review threshold.

Refunds trigger charge.refunded events. The handler marks the order as refunded in the database but does not deprovision the eSIM—customers keep connectivity, and support decides case-by-case whether to revoke access.

How I used Claude, ChatGPT, and Lovable across implementation and debugging

I used AI tools differently depending on the problem. Claude handled architecture exploration and migration scripts—I'd paste my WordPress database schema and ask it to generate SQL for transforming orders into the new Supabase structure. It caught edge cases like partial refunds and guest checkouts that I hadn't considered.

ChatGPT was better for debugging webhook race conditions. When duplicate eSIMs were provisioning, I fed it the Stripe event logs and my handler code. It spotted that I was checking order status before acquiring a transaction lock, which let two webhooks pass the guard simultaneously.

Lovable generated React components for the customer dashboard and checkout flow. The initial output needed refactoring—it used client-side API calls that exposed credentials—but it gave me working UI in minutes instead of hours.

Where AI failed: security boundaries. Every tool suggested putting eSIM provider API keys in environment variables accessible to the browser. I had to manually architect server actions and route handlers to keep credentials isolated. AI also couldn't reason about Supabase RLS policies interacting with foreign key constraints—those required trial and error with actual data.

What AI got wrong and where human engineering judgment remained essential

AI tools accelerated development, but they also confidently hallucinated solutions that would have broken production. Claude suggested wrapping the entire checkout flow in a try-catch without proper error boundaries—a pattern that would have silently swallowed payment failures. ChatGPT generated a webhook handler that called supabase.from('orders').update() without checking RLS policies, which would have failed for every customer-facing request.

The eSIM provider API integration required the most human oversight. AI-generated code assumed synchronous provisioning, but the actual API returned a pending state that required polling. I had to architect a background job system with exponential backoff and failure notifications—something AI never proposed because it didn't understand the business impact of a customer waiting 20 minutes for connectivity.

AI also struggled with data migration edge cases. It generated a script to import WordPress orders but didn't account for refunded orders with partial eSIM usage, orders split across multiple payment attempts, or customers who changed email addresses between purchase and delivery. I spent two days writing validation logic and dry-run reports that AI never suggested.

The pattern I learned: use AI for scaffolding, refactoring, and exploring unfamiliar APIs. Then apply engineering judgment to error handling, state management, security boundaries, and anything involving money or customer data.

Testing, Observability, and Recovery Strategies

A production eSIM platform needs to recover gracefully when things break. I built monitoring around three failure modes: Stripe webhook delivery failures, eSIM API provisioning errors, and partial order completion where payment succeeded but provisioning didn't.

I added a provision_attempts column and last_error field to track retry state. When an eSIM provision fails, the system logs the error and marks the order for manual review. A daily cron job queries orders stuck in "payment_complete" status for more than 10 minutes and sends me a Slack alert with the customer email and error message.

For observability, I use Supabase's built-in PostgreSQL logs and Vercel's function logs. Every eSIM provisioning attempt logs the provider response, order ID, and timestamp. This made debugging webhook race conditions possible—I could see two provisioning calls hitting the same order within milliseconds.

I also built an admin panel that lists orders by status with direct links to Stripe payment details and eSIM provider dashboards. When a customer contacts support about a missing eSIM, I can trace the entire flow in under 30 seconds: payment received, webhook processed, API called, QR code generated.

The platform hasn't had a duplicate provisioning incident in production since adding the unique constraint and attempt tracking.

Preserving SEO and building content clusters for African eSIM comparisons

The old WordPress site had accumulated organic traffic through comparison pages like "best eSIM for Kenya" and "South Africa vs Tanzania eSIM." I couldn't afford to lose those rankings during the rebuild, so I mapped every indexed URL and preserved the slug structure in Next.js.

I used AI to generate the initial migration map by feeding it the WordPress sitemap and asking it to produce a CSV with old URLs, new routes, and redirect rules. Claude caught about 90% of the patterns, but I had to manually fix product category URLs that had changed between the old WooCommerce taxonomy and the new Supabase schema.

For content clusters, I built a simple MDX-based system where each country page pulls live pricing from Supabase and renders comparison tables. The AI tools helped structure the data model—each eSIM product links to supported countries, and each country page queries active products filtered by coverage and sorted by price. This replaced dozens of manually updated WordPress pages with a single dynamic template.

I set up next-sitemap to regenerate the sitemap on build, and added next/headers redirects for legacy URLs. Google Search Console confirmed zero dropped pages after the migration went live.

FAQ: Creating an eSIM Business, Provider Access, Regional Restrictions, and Common Failures

How do you get access to eSIM provider APIs?

Most eSIM aggregators require a business entity, tax documentation, and proof of a customer-facing platform. I started with Airalo's reseller program, which has a lower barrier to entry than direct MNO relationships. Providers typically grant API credentials after reviewing your integration plan and business model. Expect 1–2 weeks for approval and sandbox access before production keys.

What regional restrictions affect eSIM delivery?

Some countries block eSIM activation entirely (China requires local registration), and certain providers restrict which regions you can resell. Your platform needs to handle region-based product visibility and clear error messages when a user's destination isn't supported. I store provider coverage metadata in Supabase and filter products at query time based on the user's selected destination.

What are the most common eSIM provisioning failures?

Webhook timeouts, duplicate activations from retry storms, and QR code delivery failures dominate my error logs. The idempotency patterns I described earlier solved duplicates. For delivery, I added a fallback: if email fails, the customer can retrieve their QR code from their account dashboard by querying esim_orders with RLS enforcing ownership.

Top comments (0)