Work / Web app · 2026

Rebuilding an AI headshot platform from WordPress plugins to Go

Adazing's AI headshot platform trains a personal model on a user's photos and generates professional headshots from a library of styles, for two brands on one backend. I built it first as WordPress plugins, then rebuilt it as a Go API and worker on PostgreSQL and Redis queues, with a Next.js front end, provider fallbacks and Stripe payments. Every commit is mine, and the platform is live.

  • case study
  • live
  • employer
  • web app
  • client anonymized
Client
AI headshot platform (Adazing)
Type
Web app · SaaS: AI headshot generation
Built with
Go, chi, PostgreSQL, Redis, Asynq, TypeScript, Next.js, React, Tailwind CSS, OpenAPI, Docker Compose, Caddy, Astria API, OpenAI API, Wavespeed, Stripe, Bunny CDN, Resend, PHP, WordPress, Gravity Forms
Also uses
Vercel, Neon
Last active
2026

Overview

An AI headshot platform for Adazing: a user uploads photos, a personal LoRA model is trained on them, and the app generates professional headshots from a library of style prompts. A book-cover mode combines the user with their own book cover, and two brands run on one multi-tenant backend.

I built it twice: first as WordPress plugins, then as a Go API and worker with a Next.js front end that replace them. It is live.

Context and problem

The first version ran as PHP plugins inside WordPress: one Gravity Form took the training photos, another captured a book cover, and AJAX and shortcode UIs, a credits class and the AI webhook receivers all ran inside WordPress requests, with assets on Bunny.

Training and generation calls are slow, and they ran inside those requests. The rebuild's README calls queued workers "the core fix for WordPress's synchronous slowness".

The audience appears to be people who want professional headshots without a photo shoot, including authors, judging by the book-cover mode.

My role

I built it as an Adazing employee. Every commit in both git repositories is mine: 18 in the Go and Next.js rebuild and 8 in the earlier WordPress plugin repo, with no merges.

In the rebuild that covers the Go API and worker, the PostgreSQL schema, the Next.js front end, the OpenAPI contract and the deployment configs.

Three of the plugin commits record changes that another person requested or supplied.

The later plugin suite has no git history. It appears to be my work too, since it continues my plugin repo and is the spec the rebuild ports.

Constraints and requirements

  • Two brands, one backend: every API route resolves a tenant from a header or the host, and both brands are seeded.
  • Old asset URLs keep working: Bunny keeps the legacy per-model path convention.
  • The style library moves over: about 3,500 WordPress style posts and their preview images had to come across into PostgreSQL and Bunny.
  • WordPress keeps selling: buyers still start on the WordPress site, which links to Stripe Payment Links.

Research and planning

The rebuild keeps a read-only copy of the WordPress plugins as its behavioural spec, and the move is phased: the first brand ships first, while the second is tenant-ready from day one and migrates later.

Solution and user experience

A user uploads photos straight to the API, a personal model is trained on them, and they then generate headshots from styles in the library and keep the results in a gallery.

The web app has dashboard, training, models, generation, gallery, library, collections, book-cover, account and admin pages, plus sign-up, login and password flows.

Credits are bought on the WordPress site through Stripe Payment Links. After paying, a new buyer lands in the app, is signed in and is asked to set a password.

Key features

  • Training pipeline: direct photo uploads, Astria LoRA training, renewal and inspection, with status reported back by webhooks.
  • Provider routing: one pure, unit-tested function uses the trained model when it is valid and falls back to gpt-image-2 when it has expired or is missing, with admin "force" modes.
  • Queued AI work: slow AI jobs run on Asynq workers backed by Redis, off the request path.
  • Style library: style prompts migrated from WordPress by a resumable importer and sorted into collections by an OpenAI classifier.
  • Book-cover mode: combines the user with their book cover.
  • Payments: Stripe Payment Links, with credits granted by one idempotent function that both the success redirect and the webhook call.
  • Security: JWT in httpOnly cookies behind a Next.js proxy, password-reset tokens, rate limiting and signature checks on the Stripe, Astria and Wavespeed webhooks.

Architecture and stack

Before: PHP plugins inside WordPress, built on Gravity Forms hooks, shortcodes, a WP-CLI command class, AJAX UIs and webhook receivers, all running synchronously in WordPress requests.

After: one Go module with two binaries, an API and a worker, so the provider clients exist only once; PostgreSQL is the store and Redis with Asynq is the job queue.

  • API: Go 1.23 with chi routing, pgx and golang-jwt.
  • Data: PostgreSQL through six embedded SQL migrations covering tenants, users, models, generation jobs, assets and their versions, the prompt library, collections, auth tokens, Stripe events and purchases.
  • Front end: Next.js 14 App Router in TypeScript with Tailwind, typed from an OpenAPI contract with openapi-typescript.
  • AI services: Astria for LoRA fine-tuning, OpenAI for gpt-image-2 image edits and prompt classification, and Wavespeed for attribute extraction.
  • Other services: Bunny Storage and CDN for assets, Stripe for payments and Resend for email, each behind its own client package.
  • Deployment: configured two ways, as Docker Compose on a VPS with the datastores not exposed, or as a DigitalOcean droplet with Neon Postgres and Caddy for HTTPS; the front end deploys to Vercel.

Implementation details

The Next.js app proxies browser calls to the Go API, forwarding the httpOnly cookie and adding the tenant header, so the browser needs no CORS and the cookie stays httpOnly. Photo uploads were later moved to go straight to the API, using CORS and a cookie shared across the domain.

The library import is decoupled from WordPress: an exporter writes NDJSON, and the Go importer re-hosts each preview on Bunny and upserts a row keyed on the WordPress post, skipping images it has already copied, so it can be re-run safely. With the previews on Bunny, the new app has no runtime dependency on WordPress.

The success redirect usually reaches the API a second or two before the Stripe webhook. Both call the same provisioning function, which is idempotent on the checkout session, so the race is safe.

A re-runnable bootstrap command creates the Stripe product, one-time price and Payment Link for each plan in one catalog and registers the webhook. It reuses what already exists, has a dry run, and refuses to run when the live flag and the key's mode disagree.

A livemode check stops a test endpoint from minting real credits. Auto-login after payment works only once, within 15 minutes, for a brand-new account with no password, and accounts are never merged on email alone.

Both binaries run the embedded migrations on boot, and a Postgres advisory lock serialises them.

Challenges

  • Long AI calls inside WordPress requests made the plugin product slow; moving them onto queued workers was the core fix.
  • Getting Astria training right took several rounds within a few days: branch and default fixes, tune configuration, a webhook parser, a trigger-word fix and changes to the tune characteristics sent in prompts.
  • About 3,500 library posts and their preview images had to move without breaking existing URLs.
  • The plugins changed AI providers more than once, from Astria to a Wavespeed and fal flow with an optional GPT image engine. The rebuild went back to Astria for training, with gpt-image-2 as the fallback and Wavespeed kept only for attribute extraction.

Trade-offs

Selling through Payment Links on the existing WordPress site likely kept the storefront as it was and avoided an in-app checkout, at the cost of credits arriving by two paths, the redirect and the webhook, which the single idempotent provisioning function handles.

Client work: links and code are not public.

Skills demonstrated

  • Backend development in Go, with chi, pgx and PostgreSQL.
  • Background job queues with Asynq and Redis.
  • Applied AI integration: LoRA training, image generation and provider fallback.
  • Legacy migration from WordPress plugins to a service architecture, including a resumable data import.
  • Multi-tenant API design.
  • Payment integration with Stripe and idempotent provisioning.
  • Web security: httpOnly JWT cookies, rate limiting and webhook signatures.
  • Next.js front-end development against an OpenAPI contract.
  • WordPress plugin development in PHP.
All projects →