Product Admin (internal)
The Product Admin is where products, prices, licenses and community access are managed. It replaces SQL migrations and copied IDs. The license database (littlebig-licenses) is the single source of truth. Paddle, FluentCommunity and EmDash are kept in step from it.
Address: https://products.andredaus.com
Repo: littlebigco/littlebig-products-admin (a Cloudflare Worker, littlebig-products-admin)
Deploy: CLOUDFLARE_ACCOUNT_ID=24c012a99395420605c00d2a39896dd0 bunx wrangler deploy
Access
Section titled “Access”The page is protected by Cloudflare Access (WARP device posture) and the Worker also verifies the Access token itself. If the Worker secrets ACCESS_TEAM_DOMAIN and ACCESS_AUD are missing it answers 503 to everything, so a mistake in Access never leaves the page open. Because the policy checks the device and not a person, the audit log records the editor as device:xxxxxxxx unless the policy also requires an email.
| Secret / binding | Purpose |
|---|---|
ACCESS_TEAM_DOMAIN, ACCESS_AUD |
Verify the Cloudflare Access token (Worker secrets) |
ADMIN_TOKEN |
Calls the license server from the server side (shared Secrets Store entry, never sent to the browser) |
Products and tiers
Section titled “Products and tiers”- Create a product with a name, a slug (lowercase letters, digits, hyphens), a type (course, plugin, service) and a first tier. Slugs are unique.
- Sold through Paddle? The switch decides whether the product gets prices in Paddle. Internal products (for example the community bridge) are set to No: they never get a Paddle price and never appear in the public catalog.
- Trial days are per tier. They are passed to Paddle as the trial period, and only on recurring prices.
- Archive instead of delete. Paddle cannot delete products and licenses reference them. Archiving hides the product, stops it being sold, archives its Paddle prices and keeps every license working. There is no delete route.
- Extra fields. Products and tiers have a free-form
attributesfield, so a new field needs no migration.
Prices (Paddle)
Section titled “Prices (Paddle)”Prices are created in Paddle from the tier page (“Add price in Paddle”): an amount in EUR, tax-exclusive, and one-time, monthly or yearly. The Paddle product is created once, with the first price, and reused.
- Changing a price creates a new Paddle price and archives the old one, so current subscribers keep what they signed up for.
- Renaming a product updates its name and description in Paddle too.
- Each step is recorded (the “Paddle status” panel). A failed step shows where it stopped; pressing the button again continues without creating anything twice.
- Refresh amounts from Paddle (home page) only reads: it fills in amounts for prices that were created before the catalog existed.
- Environment. Everything runs against the Paddle sandbox (
PADDLE_ENVIRONMENT = sandbox). There is no Paddle production account yet, and a “publish to live” step does not exist yet.
Community (FluentCommunity)
Section titled “Community (FluentCommunity)”A product with a community kind (course or space) gets its course or space created in FluentCommunity when the product is created. The “Create in community” button on the product page retries it safely.
- Courses are created as a private draft. Drafts are not shown in the public list: open the course management area, find it with the Draft filter, add the lessons and publish it there.
- The course is assigned to André’s WordPress account (
COMMUNITY_COURSE_OWNER_ID = 1). FluentCommunity only shows each person the courses they own or manage, and API-created courses would otherwise belong to the API user. - If the slug is taken, FluentCommunity appends a suffix. The catalog stores the real slug and the tier grants that one.
Licenses
Section titled “Licenses”The Licenses page lists every license with search (email, name, invoice reference, note, key) and filters for product, source and status.
- New manual license for customers who pay by invoice: product and tier, customer email and name, optional end date (empty means until revoked), invoice reference, note. It enrols the customer in the course or space the tier grants.
- Extend sets a new end date and reactivates an expired or revoked license.
- Revoke removes the customer’s community access, unless another active license of theirs still grants the same course or space. Removing someone from a course also deletes their progress.
- Only licenses with source
paddleare read-only, because Paddle events rewrite them. Manual, imported (fluentcart_migration), internal and test licenses can be edited. - Every create, extend, revoke and edit is written to the audit log (
admin_audit_log).
Email address check
Section titled “Email address check”The address is checked before anything is created. It blocks invalid syntax, disposable providers, and domains that do not exist or have no mail server (UserCheck, cached in the KV namespace shared with the auth server and newsletter, with a DNS lookup as the fallback). It warns on unusual characters before the @ and on typos of popular providers (“Did you mean gmail.com?”); a warning needs the “use it anyway” tick. A mailbox that does not exist at a real domain cannot be detected without sending to it.
A Paddle buyer with an unusable address still gets the license (they paid), flagged ATTENTION in the note, but no key email is sent, no community account is created, and an email report goes to the administrator.
Daily check (Reconcile)
Section titled “Daily check (Reconcile)”The Reconcile page compares licenses, Paddle and the community and repairs drift.
- Check now reports only and changes nothing. Check and apply fixes applies what the check found.
- It enrols buyers of courses who are missing. It never enrols anyone into a space: joining a space is opt-in through the bridge.
- It removes a member only if the system added them (an active row in
community_grants), no active license covers them any more, and at mostRECONCILE_MAX_REMOVALS(5) people would be removed in one run. Beyond that nothing is removed and the report says so. People added by hand are never touched. - A course or space that cannot be read is skipped as a whole, so an outage can never look like “nobody is a member”.
- It compares Paddle subscription status with licenses: a license that is active while Paddle says canceled is canceled when applying. The reverse (Paddle active, license not) is only reported.
- An email is sent only when the run found something.
- Access is also removed by events, without waiting for the check: a Paddle subscription cancellation, and a license revoked in the admin. A license that simply expires (its end date passes) is only caught by the check.
- The scheduled run is off. The Cloudflare account is on the Workers Free plan, which allows 5 cron triggers in total (all used). After upgrading to Workers Paid, uncomment
[triggers]inpackages/license-server/wrangler.tomland deploy.RECONCILE_APPLYstays"false"until the reports look right, and the scheduled run then only reports.
All mail from the license server (reports, license-key emails, the contact form) goes through toSend from licenses@notify.littlebig.co, with replies to the contact address. The key is the Secrets Store entry tosend-api-key. ZeptoMail is no longer used here. The “Send test email” button on the Reconcile page sends one mail and says which provider delivered it; if toSend failed, it shows the reason.
Only plugins get a license-key email. Course and service buyers do not.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and fix |
|---|---|
| Every page answers 503 | ACCESS_TEAM_DOMAIN or ACCESS_AUD is missing on the Worker |
| “Create in community” reports a failure | The message names the cause. Credentials are the Worker secrets COMMUNITY_API_USER / COMMUNITY_API_PASSWORD (a WordPress Application Password) |
| A course exists but is not visible in the community | It is a draft, and it is only visible to its owner; check the Draft filter in the course management area |
| Test email says it failed | Read the reason shown. The sender domain must be verified in toSend (notify.littlebig.co) and tosend-api-key must exist in the Secrets Store |
| Reconcile says “removal brake tripped” | More than 5 removals were planned. Look at the list before applying again |