AZ
Back to constellation
Web3Draft

GKOI Apps

The GKOI frontends — a Next.js 16 admin dashboard, the public mint site, and an NFT gallery — that turn chain operations into an interface a human can drive.

Next.js 16React 19TypeScriptPrivy + wagmi + viemTanStack Query / SWRTailwind v4Playwright

Overview

What it is

The user-facing tier of GKOI: three Next.js 16 (App Router) + React 19 apps on a Privy + wagmi + viem wallet stack (READMEs mentioning RainbowKit are stale — there is no RainbowKit in any manifest).

AppRoleStack shape
gkoi-admin-v2Operator dashboardPrivy + wagmi + viem, TanStack Query + SWR, react-hook-form + Zod, Playwright e2e
gkoi-client-v3Public mint siteLean read/submit: wagmi + viem + SWR + axios
gkoi-galleryNFT galleryLean read-only browse/detail

None of them owns any source of truth: collection/config/whitelist data is fetched from the platform services as the same typed IResponseData<T> envelope the services expose, and the server re-authorizes and re-enforces every mutation. The frontends are projections.

The design move that makes chain ops legible

The admin's core idea is counter-intuitive: for almost every privileged task, the operator does not sign a raw wallet transaction. Instead the action is a labeled button that POSTs to a backend the operator is already authenticated to, and the chain/indexing work happens server-side. The REST client is a thin cookie-auth axios wrapper — no bearer token ever sits in JS-readable storage:

const axiosClient = axios.create({ withCredentials: true });

Each action drives UX through a toast.loading → toast.update(success|error) lifecycle and surfaces the server's own error message verbatim, guarded by an isUserLoggedIn check:

const toastId = toast.loading("Adding new collection...");
const url = `${env.MAIN_SERVICE_URL}/api/collections/add/${tokenAddressOrSlug}?chainId=${chainId}`;
const { status, data: { data } } = await api().post(url, null);
if (status !== 201 || !data) throw new Error();
toast.update(toastId, { render: "Successfully added new collection!", type: "success", isLoading: false, autoClose: 3000 });

So the operator clicks a clearly-named action, watches a live loading→result toast, and never has to reason about calldata or gas for indexing an NFT collection, reindexing it, or deleting it.

On-chain state as read-only UI

Contract state is surfaced as plain dashboard values via wagmi useReadContracts/useReadContract — no transaction required to look. The presale hook reads owner, stage, and stagePrices, then maps the raw stage enum to a human stageName and a formatEther price:

const { data: contractReadsResults } = useReadContracts({
  allowFailure: true,
  contracts: [ { ...presaleContract, functionName: "owner" },
               { ...presaleContract, functionName: "stage" } ],
});

The one genuine chain write

There is exactly one real direct chain write in the whole admin: the swap token redeem/claim, via wagmi useWriteContract, which submits the user's own signed claim(...) through their connected wallet — never a server-held key. It's the deliberate exception that proves the rule that privileged mutations are otherwise delegated to the authenticated backend.

const result = await mutateAsync({
  address: (siteConfigs?.gKoiSwapAddress as `0x${string}`) || "",
  abi: gKoiMultiSwapABI, functionName: "claim",
  args: [payload.signatureData?.signature || "", id, payload.amountWei, payload.deadline],
});

Auth gate and operator breadth

Login is Privy (SIWE-style) → an HttpOnly session cookie (/api/login|verify|logout route handlers) → an admin-role/permission verify, with a tri-state gate (documented in the app's own flicker-design doc) built so the connect/sign dialog never dead-ends when unauthenticated. Admin role is fetched from gkoi-authentications — the frontend never decides authorization itself — and RBAC hooks (useAdminAccessControl, useHasPermission, useIsSuperAdmin) gate a broad operator surface: manage_collections, certified_nfts, art_contest_*, communities, gcoin, guardians, power, quests, permissions, announcements, admin_audit_log, gallery_whitelist, arenas, fighters, and more.

Testing & security posture

The admin ships Playwright e2e (playwright.config.ts, tests/) over the operator routes; all three apps run Biome + ts.check + lint-staged and use patch-package. Authorization is server-side by construction — cookie auth keeps tokens out of JS, and the few chain writes go through the operator's own wallet.

Honest note

On the public mint site, the actual on-chain buyPresale submit path is currently commented out behind a "SOLD OUT" state — the code even carries a note that it was left in place "so the presale can be re-enabled without rebuilding it." So a live mint flow is not claimed as active here; what ships today is the eligibility read, the claim-signature plumbing, and a SOLD-OUT UI.

Related write-ups

Write-up in progress

A companion post for this project is being drafted. Check back soon.