All projects
AI SaaS platform2024 — 2025Product work · employed

An AI platform people can actually subscribe to

A subscription SaaS platform for fine-tuning and deploying AI models. I led the front end and built the authentication, billing, data pipelines and admin tooling behind it.

Bagel Labs interface

Bagel Labs needed a platform where users could fine-tune AI models and pay for access to advanced image and video models such as VEO 3 and Flux. That meant three hard things at once: secure accounts, reliable subscription billing, and a workflow for uploading and processing large datasets — all wrapped in an interface that a technical audience would trust.

I joined as a full-stack developer and led the front end, owning the front-end architecture and building across the stack.

Authentication and access control. A secure authentication system using NextAuth.js with OAuth providers (Google, GitHub) and role-based access control, separating what general users and admins can reach.

Subscriptions and billing. Stripe integration handling subscription plans, upgrades, billing workflows and transaction history — the mechanism that turns the platform into a business.

Model deployment and fine-tuning. A workflow for deploying, testing and fine-tuning image, video and text models, with real-time monitoring so users can see the status of a job rather than guessing.

Data and file management. Upload, processing and validation pipelines for the datasets users submit for fine-tuning, backed by AWS S3 and DynamoDB.

Admin console. A dashboard for user management, system monitoring and the operational controls the team needed to run the platform day to day.

Front-end architecture. Responsive, accessible components built on Radix UI and modern React patterns, set up so new features could be added without redesigning what already existed.

The code belongs to Bagel Labs, so what follows is the architecture and the decisions behind it rather than the source.

One Next.js application, three trust boundaries. The public marketing surface, the signed-in product, and the admin console live in the same App Router codebase but are separated by where authorisation is enforced: on the server, on every request, from the session — never by hiding a link in the client. NextAuth issues the session; the role on it decides what a request is allowed to touch.

Billing state comes from Stripe, not from the app. A user's plan is whatever Stripe's webhooks say it is. Checkout starts a session, the webhook updates the subscription record, and every gated feature asks that record — not the client, and not the result of the checkout redirect, which a user can never be trusted to complete. That is what makes upgrades, downgrades, failed payments and cancellations behave correctly without a nightly reconciliation job.

Datasets never travel through the API. Fine-tuning datasets are large. The browser uploads straight to S3, and the application only handles the small things: issuing the upload credential, recording the object, and validating the result. The dataset and job records live in DynamoDB, keyed by owner, because the access pattern is "everything belonging to this user, newest first" — a key-value read, not a query that needs joins.

Jobs are long, so the interface is built around status, not completion. A fine-tune or a deployment can run for a long time; the UI treats every job as a row with a state and a timestamp that updates as the run progresses, so a user can leave and come back rather than sit on a spinner.

The admin console is the same application with a different role. User management, monitoring and the operational controls are routes that require the admin role server-side — not a separate deployment, which for a small team would have meant two codebases drifting apart.

No screenshots: the platform is Bagel Labs' product and the console shows real customer accounts. What it had to answer, and how each answer was built:

Who is this user, and what are they entitled to? One page per account: profile, role, plan and subscription status read from the billing record that Stripe's webhooks maintain — so support is looking at the same source of truth the paywall uses, not a cached copy that can disagree with it.

Why did this job fail? Fine-tunes and deployments are listed with their state, owner and timestamps, so a failure is a row to open rather than a log to search. The same states the user sees, without the marketing around them.

What is this account storing? Datasets with their validation results, which is where a "my upload doesn't work" ticket is usually answered in one look.

Is the platform healthy right now? Monitoring for the operational picture — what is running, what is queued, what is erroring.

Two rules held the console together. Every action is authorised on the server from the session role, so an admin route is not reachable by guessing a URL. And admin actions are not silent — anything that changes another account leaves a record, because "who changed this plan" is a question that always gets asked eventually.

The hard part was not any single feature — it was keeping fast iteration and long-term scalability in the same codebase. Fine-tuning workflows, subscription gating, cloud infrastructure and authentication all touch each other, and a shortcut in one shows up as a bug in another. Getting that right end to end, in a fully remote team, is the part I carried into every project since.

05 — Contact

Open to full-time full-stack and frontend roles — in Riyadh or remote — and to freelance builds. The quickest way to reach me is email; I reply to everything.