v-va*

← return to the collection

02 · license platform

cp center

from storefront to activation — software licensing in one building

saas · fullstack platform


artist
fazza abiyyu
date
2026
role
fullstack development — ui, api & integration
medium
Svelte 5 · Elysia · Bun · Prisma · PostgreSQL
status
on view

statement

The licensing platform behind CP Center — a local-first desktop document suite. Buyers order seats, pay via QRIS, and receive keys that admins can track down to devices and activations. Svelte 5 frontend wired 1:1 to an Elysia API on Bun, with a shared OData query language, silent token refresh, and an order lifecycle that issues licences automatically.

  • portals two worlds, one app: admin back-office and buyer portal under shared auth.
  • lifecycle orders expire lazily after 24h; approval auto-issues one licence per item.
  • queries odata-style $filter / $orderby / $search built client-side, honoured server-side.
11 api domains
2 portals, one shell system
24h payment deadline
0 runtime dependencies (fe)

the problem

licensing is paperwork until engineered

Selling licensed software means juggling orders, payment proofs, seat counts, device activations and suspensions — usually across spreadsheets and chat threads. Buyers have no self-service, admins have no single source of truth.

the answer

one platform, whole lifecycle

CP Center gives buyers a store, a wallet of licenses and live order tracking; admins get customers, licenses, activations, devices, products and revenue dashboards. Payment proofs upload as AVIF to Cloudinary, approvals issue licences automatically, and every list speaks the same OData dialect.

exhibited artifact

fig. i — the public face · live at cpcenter.biz.id
fig. i — the public face · live at cpcenter.biz.id
fig. ii — the control room · admin dashboard · svelte 5 runes
fig. ii — the control room · admin dashboard · svelte 5 runes
fig. iii — the registry · license management · seats & devices
fig. iii — the registry · license management · seats & devices
fig. iv — the ledger room · orders · qris proof workflow
fig. iv — the ledger room · orders · qris proof workflow
fig. v — the buyer side · portal · my licenses
fig. v — the buyer side · portal · my licenses

requirements analysis

functional requirements

coderequirementdescription
FR-01Auth & SessionsJWT access + refresh tokens; single-flight silent refresh on 401
FR-02Customer ManagementCRUD with archive (soft delete) keeping licence references intact
FR-03License RegistryIssue, suspend, revoke and archive licenses with per-seat meters
FR-04Devices & ActivationsTrack devices per license; deactivate/reactivate from admin or buyer scope
FR-05Ordering FlowStore → PENDING order → QRIS payment → proof upload → approve/reject → licences issued
FR-06Payment ProofBuyer uploads transaction screenshot (converted to AVIF via Cloudinary); reject carries a reason
FR-07DashboardsSummary counters, merged activity feed and revenue buckets (30d/90d/YTD)
FR-08OData Queries$top/$skip/$count/$orderby/$filter/$search accepted in both casings, capped server-side
FR-09NotificationsIn-app bell fed by payment submissions, approvals and rejections
FR-10Role ScopingBUYERs see only their own licenses/devices/dashboard; registration always yields BUYER

materials list

technical inventory

frontend Svelte 5 (runes)
bundler Vite
language TypeScript
backend Elysia (Bun)
orm Prisma
database PostgreSQL
auth @elysiajs/jwt + refresh
media Cloudinary (AVIF)
styling Design tokens, plain CSS
api style REST + OData query options
email Mailjet / Mailtrap mocks
deploy Vercel

notable installations

features & integrations

payments without a gateway

The Order Lifecycle

Orders land as PENDING with a live 24-hour countdown. Buyers pay by QRIS, upload the transaction proof (atas nama + screenshot → AVIF on Cloudinary), and every admin gets notified. Approve moves it to PAID and issues a licence per item automatically; reject returns it with a visible reason. Expired orders cancel themselves, lazily.

odata-style lists

One Query Language Everywhere

A shared ListState owns search, sort, filter and pagination for every table. The UI builds OData options ($filter with eq/ne/gt/contains…, $orderby, $top capped at 200) that both casings satisfy — unknown fields are ignored instead of exploding.

silent refresh

Sessions That Repair Themselves

~15-minute access tokens with ~7-day refresh tokens. On any 401, one single-flight refresh call runs (parallel requests share it), the failed request retries, and only a true failure ends the session at /login.

native fetch only

Zero-Runtime Frontend

No HTTP client library: one centralised api.ts unwraps { value } envelopes, converts error envelopes into readable messages, and maps snake_case DTOs into camelCase models so components never touch raw API shapes.