Skip to content
Bassim Wrak
All projects
Case studyWeb agency - site production2025

Admin console for a WordPress site portfolio

One interface to assign sites and tools across a team, notify in real time, and open a WordPress session without ever sharing a password.

Role · Design, WordPress back end and Next.js front end (solo)

Site list: name, category, assigned contributor and link, on demonstration data
  • 0

    passwords ever handed to a contributor

  • 6 resource types managed from a single screen

    resource types managed from a single screen

  • 4 sources folded into one search

    sources folded into one search

Client
Internal project
Period
September - December 2025
Duration
4 months
Deliverables
  • Admin console: accounts, sites, categories and tools in one place
  • Walled contributor space - each person sees only what was assigned to them
  • Delegated WordPress session opening, with no credential handed over
  • Real-time notifications on every access granted or revoked
  • Global search covering accounts, sites, categories and tools

The problem

An agency maintaining several dozen WordPress sites hits the same wall: every site has its own credentials, every licensed tool its own key, and all of it travels by chat and email. Access is shared between contributors without anyone knowing precisely who holds what. When someone joins or leaves, the passwords stay in circulation, and nothing records which sites they worked on.

This application answers that with a single console. An administrator declares sites, categories and tools, then assigns each contributor exactly what they need. Contributors see only what was assigned to them, and open a session on a site without ever receiving its password.

The structural choice is decoupling: WordPress stays the source of truth - it is already the working tool, and a dedicated plugin exposes assignments, notifications and search. The Next.js application holds no database of its own; it is a presentation head and an orchestration layer over the REST API.

Ten modules, ten problems solved

ModuleWhat it solves
Accounts and rolesOpen, change or close an access from a single screen; closing an account strips every assignment attached to it at once.
SitesThe portfolio described once and for all - address, category, credentials - then filtered: each person sees only their own.
CategoriesA classification that survives deletion: sites in a removed category fall back to an uncategorised state instead of vanishing.
Tools and licencesLicence keys and shared accounts stop travelling by chat: they are assigned exactly like a site.
AssignmentsGrant or revoke an access in two gestures, with no duplicate possible: the selection is compared against what exists before any write.
Session openingA single-use link opens the site's dashboard without any password changing hands.
NotificationsEvery access granted, revoked or used surfaces in real time: activity across the portfolio stops being invisible.
Global searchOne panel queries accounts, sites, categories and tools - no need to know which screen to look in.
PresenceA heartbeat every thirty seconds shows who is working, without anyone having to declare it.
DashboardPortfolio counters stay visible throughout navigation, including whatever is still assigned to nobody.

The screens, on demonstration data

  • Every account carries its role and the sites entrusted to it. Closing the account takes the assignments with it.
  • Licences and shared accounts are administered like sites, instead of travelling by chat.
  • Deleting a category does not delete its sites: they fall back to uncategorised.

The assignment path, end to end

  1. The administrator assigns

    From the sites or tools table, they select one or more contributors. The request first compares the selection against existing assignments and writes only the difference.

  2. The assignment is written to WordPress

    The business plugin records the contributor-to-resource link and returns its identifier. The human-readable label of the resource is fetched straight after, falling back to a technical identifier if that call fails.

  3. The notification takes two paths

    It is first persisted in WordPress, which makes it available on the next page load, then pushed to the private channel of the contributor concerned. A delivery failure never cancels the assignment.

  4. The contributor opens the site

    From their list, they request a session. The application negotiates a single-use link with the target site - its token expires after five minutes - and alerts administrators of the access, with no password ever passing through the interface.

Architecture decisions

The starting template mandated a dedicated database, with its object-relational mapping layer and migrations. Yet the sites, users and categories already lived in WordPress.
Options considered
Copy the data into a database of our own and keep it in sync, or treat WordPress as the single source of truth and write nothing locally.
Decision
Single source of truth in WordPress. The template's persistence layer was removed outright: no object-relational mapping dependency, no schema, no migrations.
Why
Two copies of the same record means permanent synchronisation work and a whole class of drift bugs. The entities already live in WordPress, where teams administer them anyway. The displaced cost is real and accepted: every screen depends on remote API latency, which client-side caching absorbs.
An assignment has to surface on the contributor's screen without a reload, but the infrastructure had no socket server.
Options considered
Poll the server on a fixed interval, run a socket server, or lean on a managed delivery service.
Decision
Managed delivery service, with one private channel per user. Interval polling is explicitly disabled; only a refresh on tab refocus remains.
Why
Polling burns requests continuously for a rare event, and a socket server would have been one more thing to operate for a single feature. The fallback is explicit: if the subscription does not land within four seconds, the cache is invalidated and the data refetched - the feature degrades rather than stalls.
Marking a notification read, or deleting one, means a round trip to WordPress. The wait was noticeable on repetitive gestures.
Options considered
Wait for server confirmation before repainting, or apply the change immediately and correct on failure.
Decision
Optimistic updates across all four list mutations, capturing prior state, restoring it on error and reconciling every time.
Why
For low-stakes, high-success gestures, the wait costs more than the occasional rollback. The catch is discipline: without restoring the captured state, a failure would leave the interface persistently wrong.
The presence indicator needed state shared across requests, with no external store provisioned.
Options considered
Add a distributed in-memory key-value store, a dedicated table, or accept a per-process registry.
Decision
Per-process in-memory registry, swept every five minutes, with the limitation documented in the file itself.
Why
Presence is a convenience signal whose loss carries no consequence: at worst a contributor shows offline until their next heartbeat, thirty seconds later. Provisioning infrastructure for that would have been out of proportion. The limitation is real and acknowledged - the state does not survive a restart and is not shared across instances.

Known limitations

The current state is a functionally delivered project, not a hardened one. Three items remain open and identified: access control is arbitrated in the browser, with no equivalent guard on the server routes; managed-site credentials travel in clear to the client instead of staying server-side; and secrets were hard-coded during the debugging phase, which forces a rotation before any publication. Naming them is part of the same work as having written them.

The visual identity is part of the work

The tool was not only built: it was named, drawn and animated. The logo starts from a stylised fennec - an animal picked for what it suggests of alertness and speed, two qualities you want from an access system - with the organisation's initial legible inside it. Continuity with the parent identity, without being a copy of it.

The palette holds to two warm tones on a dark ground, and the type is a geometric sans in capitals: readable on screen, restrained, and content to let the logo carry the personality. The dark ground is not a fashion choice - it is a legibility decision for an interface that stays open all day.

The brand animation

Four seconds, silent. It is the application's opening screen.

Stack

  • Next.js 15
  • React 19
  • TypeScript
  • NextAuth
  • TanStack Query
  • Pusher
  • Tailwind CSS 4
  • Radix UI
  • Zod
  • WordPress REST
  • PHP
  • JWT

What changes, in three numbers

  • 0

    passwords ever handed to a contributor

    How it was measured · The session opens through a single-use link requested from the target site on click.

  • 6 resource types managed from a single screen

    resource types managed from a single screen

    How it was measured · Accounts, sites, categories, tools, assignments and notifications, each with its own screens.

  • 4 sources folded into one search

    sources folded into one search

    How it was measured · A single panel queries accounts, sites, categories and tools at once.