Back to BlogArchitecture & Engineering

Building a Next.js E-commerce Admin: Orders, Stock and Roles

Rupak Amin

Founder & Lead Engineer, RAITHub

12 min read

A Next.js e-commerce admin needs seven areas: orders, products and variants, stock, customers, refunds, staff roles and an audit log, plus CSV export. Build every change as a server action that checks a named permission on the server, because the Next.js docs say to treat server actions like public API endpoints. A focused first version is a 4–6 week fixed-scope build.

If you would rather have it built for you, see how RAITHub would build this below.

This post is about the back office: the screens your staff use every day. For the architecture of the store or marketplace itself, read the custom ecommerce and marketplace guide; for wholesale pricing, credit and buyer isolation, read B2B ecommerce platform development.

What does an ecommerce admin dashboard need to do?

An admin exists so staff can answer a customer, fix an order and keep stock true without asking a developer. Each area has one job and one rule that must not be broken.

AreaStaff need toThe rule that must hold
OrdersSearch, filter by status, view, cancel, add notes, change statusStatus moves only along allowed transitions, and each move is recorded
Products and variantsEdit titles, prices, images; manage size and colour variantsPrice and SKU are per variant; a live order keeps the price it was sold at
StockAdjust counts with a reason, see reserved versus availableAdjustments are ledger entries, never a silent overwrite
CustomersLook up a customer and their orders, handle data requestsOnly roles that need personal data can see it
RefundsRefund all or part of a payment, with a reasonNever more than was paid; safe to retry; through the payment provider
RolesGive each staff member only what their job needsChecked on the server for every action, not by hiding buttons
Audit and exportSee who changed what; export orders for accountsThe log is append-only; exports respect the same permissions

Should I use Shopify, Medusa or Payload admin, or build a custom one?

Use an existing admin when your workflow fits it. Build when your rules are the business. A few facts from the vendors' own pages:

  • Shopify admin comes with the store. Shopify's pricing page lists Basic from $19 a month (billed yearly) with additional staff accounts not included, Grow from $49 with up to 5 staff accounts, Advanced from $299 with up to 15, and Plus from $2,300 a month with unlimited staff accounts.
  • Medusa is an open-source commerce platform with an admin. Its repository states the core is MIT licensed, while "the RBAC-based Enterprise Edition materials" require a commercial agreement. Check that before you plan fine-grained staff roles on the free core.
  • Payload is an open-source, TypeScript headless CMS that you can add to a new or existing Next.js app, with access control "at the document, field, and operation level". It gives you an admin panel for your own collections; the commerce logic is yours to write.
  • Sanity is a content platform, not a commerce engine. Sanity's pricing page lists a free plan, Growth at $15 per seat a month, and custom roles only on Enterprise. It suits product content and editorial pages next to a commerce backend.

Buy, build or hire: which admin option fits?

OptionExamplesChoose this whenIt stops fitting when
Off-the-shelf hosted adminShopify adminStandard retail: one price per product, card payments, a small teamYou need custom order states, approvals, local payment rails, or roles beyond the plan's staff limits
Open-source admin or templateMedusa admin, Payload admin panelYou want to own the code and extend a ready admin rather than start from blankYour roles, stock or pricing rules fight the framework's model, or a needed feature sits in a paid edition
Custom Next.js adminApp Router, server actions, your own databaseYour order flow, stock model, permissions or audit needs are specific, or the admin must sit next to an existing custom storeYour workflow is standard; then you are paying to rebuild software you could rent

A hybrid is common: keep Shopify as the storefront and build a custom admin only for the part it does not cover, such as a fulfilment or credit-control screen that reads and writes through its API.

How do you secure Next.js server actions in an admin panel?

Check authentication and a specific permission inside every action, on the server, before touching data. The Next.js authentication guide says to "treat Server Actions with the same security considerations as public-facing API endpoints", and warns that returning null from a layout to hide content does not stop nested segments and server actions being reached. Its advice is a data access layer that centralises authorization, with Proxy (formerly middleware) used only for optimistic redirects, because "it should not be your only line of defense". If your Proxy file is not running at all, see Next.js proxy.ts not running.

Below is a minimal refund action with a named permission check, input validation, a row lock, an idempotency key and an audit entry in the same transaction. The database helpers are illustrative; the order of operations is the point.

// app/admin/orders/actions.ts
'use server'

import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { requirePermission } from '@/lib/auth/permissions'
import { withTransaction } from '@/lib/db'
import { refundPayment } from '@/lib/payments'

const RefundInput = z.object({
  orderId: z.uuid(),
  amountMinor: z.number().int().positive(), // integer minor units, never floats
  reason: z.string().trim().min(3).max(500),
  idempotencyKey: z.uuid(),
})

export async function refundOrder(raw: unknown) {
  // 1. Who is calling, and may they refund? Throws if not.
  const staff = await requirePermission('orders.refund')

  // 2. Never trust the client's shape.
  const input = RefundInput.parse(raw)

  // 3. Lock the order, check the amount, record the refund and the audit entry together.
  const refund = await withTransaction(async (tx) => {
    const order = await tx.one(
      'SELECT id, paid_minor, refunded_minor FROM orders WHERE id = $1 FOR UPDATE',
      [input.orderId],
    )
    const refundable = order.paid_minor - order.refunded_minor
    if (input.amountMinor > refundable) throw new Error('Refund exceeds amount paid')

    const result = await refundPayment(order.id, input.amountMinor, input.idempotencyKey)
    await tx.none(
      'UPDATE orders SET refunded_minor = refunded_minor + $2 WHERE id = $1',
      [order.id, input.amountMinor],
    )
    await tx.none(
      'INSERT INTO audit_log (actor_id, action, target_id, detail) VALUES ($1, $2, $3, $4)',
      [staff.id, 'orders.refund', order.id, { amount: input.amountMinor, reason: input.reason }],
    )
    return result
  })

  revalidatePath('/admin/orders/' + input.orderId)
  return { ok: true, refundId: refund.id }
}

// lib/auth/permissions.ts (server-only)
export async function requirePermission(code: string) {
  const session = await verifySession() // validated against the database, not just the cookie
  const granted = await getPermissions(session.userId) // memoised per request
  if (!granted.has(code)) throw new Error('Forbidden: ' + code)
  return { id: session.userId }
}

Two details are easy to miss. The external refund call sits inside the transaction here so a failure rolls back the local write; if your provider call is slow, move it to an outbox job and keep the idempotency key so a retry cannot refund twice. And the audit insert shares the transaction, so a change cannot happen without its record. The full audit design is in SaaS audit log design.

How should roles and permissions work in an ecommerce admin?

Use named permission codes, such as orders.refund or stock.adjust, and build roles from them. Check the code, never the role name, in your actions; then adding a "senior support" role is a data change, not a deploy. Keep sensitive pairs apart: whoever can create a refund should not also be the only person who can approve it. The design is covered in more depth in SaaS authorization and RBAC design.

The scale this reaches in a real back office: Sundor Skin, the B2B wholesale platform RAITHub built, runs 12 staff roles built from 88 permission codes, over 146 PostgreSQL tables with row-level security and 530+ automated tests.

How do you paginate large order tables in Next.js?

Use keyset (cursor) pagination for order lists, and put the filters and cursor in the URL so a page is shareable and the back button works. Offset pagination gets slower as the offset grows, because the database still walks the skipped rows, and pages shift when new orders arrive. Keyset pagination asks for "the next 50 after this one":

-- Next 50 orders, newest first, after the last row on the current page.
SELECT id, number, status, total_minor, created_at
FROM orders
WHERE status = ANY($1)
  AND (created_at, id) < ($2, $3)
ORDER BY created_at DESC, id DESC
LIMIT 50;

Back it with an index on (status, created_at, id). In the App Router, read the cursor from the page's search params in a server component and fetch there, so no order data passes through a client-side fetch you also have to secure.

When should an admin use optimistic UI?

For small, reversible changes where the server almost always agrees: a status toggle, a note, a tag. React 19's useOptimistic hook shows the new state immediately and falls back if the server action fails. Do not use it for refunds, stock adjustments or anything involving money; staff should see the confirmed result, because a screen that briefly says "refunded" when the provider declined causes real support tickets.

How should stock work in an ecommerce admin?

Record every change as a movement with a reason (received, sold, returned, damaged, counted) and derive the on-hand figure from them, or at least write a movement row alongside each update. Show reserved and available separately, so staff do not sell units already in someone's cart. Adjustments must lock the rows they change, the same way orders do. The race conditions that cause overselling are covered in inventory stock oversell prevention.

How should CSV export work?

Export through the same permission check and the same filters as the screen, so staff can export only what they can see. Stream large exports rather than building the file in memory, and write an audit entry for exports that contain customer data. Neutralise cells that start with =, +, - or @ so a spreadsheet does not treat a customer's name as a formula (CSV injection).

How long does it take to build a Next.js ecommerce admin yourself?

As a rough estimate, a developer experienced with Next.js and PostgreSQL might build orders, products, stock and basic roles in 3–6 weeks. The main risk is not the screens; it is one server action, often added late, that skips its permission check or trusts a price or amount from the browser. Write a test for each action that calls it as a user without the permission and expects it to fail.

Why RAITHub for this

  • Admins we have built and run. TheSkinProof, the founder's own skincare venture, has 5 portals on 217 API endpoints with 750+ tests. Sundor Skin's back office runs 12 staff roles from 88 permission codes. This site has its own Next.js admin panel, covered by a suite of 400+ tests, with rate limiting built without Redis (how we did it).
  • QA-first. Every permission is tested by calling actions as the wrong user. The method is in how RAITHub tests.
  • Next.js as a core stack. See our Next.js development service.

When you don't need us

  • You sell standard retail on Shopify and its admin and staff limits fit your team: stay there.
  • Medusa's or Payload's admin covers your workflow and you have a developer to extend it.
  • You need a native mobile admin app: RAITHub builds web apps and PWAs, not native apps.
  • You need a SOC 2 or ISO 27001 certified vendor: RAITHub is not certified. We sign NDAs and DPAs and work inside your controls.

How RAITHub would build this

  • Scope: orders with allowed status transitions, products and variants, and a stock ledger with reserved and available counts.
  • Scope: refunds through your payment provider with idempotency keys, and customer lookup limited by role.
  • Scope: named permission codes and roles checked in every server action, an append-only audit log, keyset-paginated lists and permission-aware CSV export.
  • Timeline: a focused admin as a fixed-scope 4–6 week build; a back office with complex stock, pricing or integrations behind an existing store typically 6–12 weeks.
  • You receive: automated tests and CI, including permission tests for every action; handover docs and runbooks; and full IP under NDA. Production data stays in your own cloud account; development uses synthetic data.

Next step: book the free 15-minute audit with your current admin or workflow, and you get a written fixed quote. The ecommerce industry page sets out what a build includes.

Frequently asked questions

Is Next.js a good choice for an ecommerce admin dashboard?

Yes, if you already run Next.js or want one TypeScript codebase. Server components fetch data on the server and server actions handle changes, but every action must check permissions itself, as the Next.js docs advise.

Are Next.js server actions secure by default?

No. They are reachable like public API endpoints. Verify the session and a specific permission inside each action, validate input, and do not rely on Proxy or hidden buttons alone.

Does Medusa admin include role-based access control?

Medusa's repository says its core is MIT licensed, while the RBAC-based Enterprise Edition materials require a commercial agreement. Check the current licence before planning detailed staff roles on the free core.

How many staff accounts does Shopify allow?

Shopify's pricing page lists additional staff accounts as not included on Basic, up to 5 on Grow, up to 15 on Advanced and unlimited on Plus.

Should an ecommerce admin use offset or cursor pagination?

Cursor (keyset) pagination for large order and customer tables: it stays fast at any depth and pages do not shift as new orders arrive. Offset pagination is fine for small lists.

How long does a custom ecommerce admin take to build?

A focused admin covering orders, products, stock, refunds and roles is typically a fixed-scope 4–6 week build; complex stock, pricing or integrations push it towards 6–12 weeks.

Next.js admin dashboardEcommerce adminServer actionsRBACNext.js App RouterAudit logMedusa

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.