Skip to content
All work

Case studyEnterprise Product Engineering2023

Community Platform for 500K+ Educators

A discussion and collaboration platform used by more than 500,000 educators across a US state, with the administration and moderation tools a public-sector deployment that size requires.

community · discussions

Moderation queue

Pending review
Discussions, with moderation in the same shellOne person's roles decide what appears in the left rail, which actions are on a post, and whether the moderation queue exists at all. Every one of those is the same permission question, asked in three places.

Interface recreated for portfolio purposes.

01

Problem to outcome

The complete case study in seven parts. The sections below provide the engineering detail behind this summary.

Problem

The platform had grown faster than its structure. Permission checks were scattered across individual components, so different screens could disagree about what the same user was allowed to do, and server data was being copied into local state to feel fast, so the screen and the server regularly drifted apart.

Context

A discussion and collaboration platform used by more than 500,000 educators across a US state, with the administration and moderation tools a public-sector deployment that size requires.

Constraints

  • One person, several roles at once
  • Moderation must be reversible and auditable
  • Sessions last all day

My role

Frontend architecture ownership: the permission layer, the data layer, and the discussion, moderation and administration interfaces. Alongside a lead designer, backend engineers, product and QA.

Approach

Every screen needs to know what this particular person may view and do. Rather than each component working that out for itself (which is how screens start disagreeing with each other), permissions are resolved once, above the features, and everything below asks the same question.

Solution

Rebuild the frontend around two decisions: resolve permissions once above the features, and give server data a single owner, so a growing team could keep adding features without re-deriving the rules each time.

Outcome

The frontend became something a growing team could work in. Permission logic has one home, server data has one owner, and features are added inside module boundaries rather than across them.
educators on the platform
products sharing one codebase and one permission model
more posts after the discussion work shipped
faster page loads
02

What is the community platform?

A community platform for public-sector educators across a US state. Teachers and school staff post questions, share classroom resources and follow topics. District and school administrators manage membership and content. A moderation team works through reported posts.

It is really three products sharing one codebase: a discussion feed, an administration console, and a moderation queue, all built on the same data and the same permission system.

The moderation loop

  1. 01

    Content reported

    Any member can flag a post

  2. 02

    Enters the review queue

    With the thread around it

  3. 03

    Moderator reviews

    In context, not in isolation

  4. 04

    Action taken

    Reversible, never a hard delete

  5. 05

    Result announced

    Including to screen readers

  6. 06

    Recorded

    Auditable after the fact

  7. 07

    Bulk actions

    The same path, applied to many

The path a reported post travels. Every step had to be reversible and auditable. A public-sector deployment cannot have a destructive shortcut in it.

What it does

Permission layer
One place that resolves a user's roles into what they can see and do, used by both page guards and individual controls.
Discussion experience
Threaded posts, groups and topic categorisation, built to stay fast as threads grow long.
Rich-text posting
An editor supporting code blocks and embedded media, with content cleaned once as it enters the application.
Moderation tooling
Reporting, content flagging, an admin review queue and bulk actions, all reversible and auditable.
Administration
Membership and content management across platform, district and school scopes.
Server-data layer
Caching, invalidation, and instant-feeling interactions that roll back correctly when a request fails.
03

Why the problem was difficult

A discussion feed is a solved problem. A discussion feed that is simultaneously an administration console and a moderation queue, for half a million people across a state, is not.

The difficulty is that one person can be a platform administrator, a district lead and a member of a single school at the same time. Those roles are additive and overlapping, and every screen, every button on every screen, has to reach the same conclusion about what they permit.

When each component worked that out for itself, screens contradicted each other, and every new rule meant re-checking the whole application. Meanwhile server data was being copied into local state to make things feel fast, so the interface and the server drifted apart in a second, separate way.

How do you let one person hold three roles at once, and still have every screen agree about what they can do?

The question the rest of this page answers
04

Contribution and ownership

Architecture

How is the application organised?

  • How data and state are organised across the whole application
  • The permission layer, and the single question features ask it
  • Module boundaries features are built inside
  • Response shapes the cache could keep in sync, agreed with backend

Product surfaces

What was delivered end to end?

  • The discussion experience: threaded posts, groups, topics
  • A rich-text editor with code blocks and embedded media
  • Moderation and support tooling, including the review queue
  • Administration across platform, district and school scopes

Data layer

Who owns the server's truth?

  • Caching and invalidation via RTK Query
  • Optimistic interactions and their rollback
  • Keeping long sessions honest against a live server

Quality

What kept it from regressing?

  • The testing strategy across unit, component, integration and end-to-end
  • The accessibility bar, and reviewing against it
  • Performance work: lazy loading, memoisation, code splitting

What isn’t mine

Backend engineers owned the services, APIs, and data model; the lead designer owned the visual design; and the product team set direction. Frontend ownership covered the architecture, interfaces, and response-shape collaboration needed to keep cached data in sync. The $2M renewal was a shared outcome across product, design, backend, and frontend engineering.

05

The constraints shaped the product

One person, several roles at once

The permission model has three overlapping scopes (platform, district and school), and a single user can hold roles in several of them simultaneously. The roles are additive, not a hierarchy you can collapse into a rank.

Permissions had to be resolved once into a full set, rather than checked as a role comparison at each call site.

Moderation must be reversible and auditable

This is public-sector infrastructure holding content written by teachers. A moderation action that cannot be undone or explained afterwards is not acceptable, regardless of how much simpler it would be to build.

No destructive shortcuts anywhere in the moderation path, and every action leaves a record.

Sessions last all day

People leave the tab open from morning to afternoon. Anything cached at login is stale by lunchtime, and a user acting on stale permissions is a security problem rather than a refresh annoyance.

Cached data revalidates in the background rather than trusting what was fetched at session start.

Accessibility is a requirement, not a pass at the end

A public-sector platform has an accessibility bar to meet on every screen, including the administration and moderation tooling that only staff ever see.

Semantic HTML first, automated axe checks inside component tests, so regressions fail in CI rather than in an audit.

The team was going to grow

Anything too clever to hand over is a liability on a platform that will outlive whoever wrote it. The architecture had to be explainable to someone who joined a year later.

Code grouped by feature with small public surfaces, and shared abstractions only once a second feature genuinely needed one.

06

How it fits together

Every screen needs to know what this particular person may view and do. Rather than each component working that out for itself (which is how screens start disagreeing with each other), permissions are resolved once, above the features, and everything below asks the same question.

  1. User

    Educator, school, district or platform staff

  2. Interface

    DiscussionsModeration queueAdministration

  3. Permission check

    Can this user do this, here?

  4. Data layer

    RTK Query cacheInvalidationOptimistic updates

  5. Backend APIs

    Owned by the backend team

Permissions are resolved above the features, so everything below asks one question instead of each screen deriving its own answer.
07

Decisions that shaped the product

Each of these solved a real problem and cost something in return. The trade-off is stated alongside the decision, because a decision without one was not really a decision.

Decision 01

Making interactions feel instant without lying to the user

Problem

Reactions, follows and replies have to feel immediate. Waiting for the server made the discussion feel slower than the content it was showing. But simply updating the screen straight away meant that when a request failed, the interface kept showing something that had not actually saved.

Decision

Update the cached data the moment the user acts, then wait for the server and roll the change back automatically if it fails. The update and its undo live together in one place, so there is no second copy of the truth to drift.

Trade-off

Each action has to know which parts of the cache it affects, so adding a new list means revisiting them. That coupling is real, but it is visible and testable, which the alternative was not.

Result

Interactions land in a single frame, failures reverse themselves cleanly, and the interface never shows a state the server disagrees with.

Decision 02

Deciding permissions in one place instead of everywhere

Problem

A user can be a platform administrator, a district lead and a member of one school at once. When components each worked out what that meant, screens contradicted each other, and every new rule meant re-checking the whole application.

Decision

Work out the user's full permission set once when the session starts, and give features a single typed question to ask: can this person do this, here. Page-level guards use the same answer as the buttons inside them.

Trade-off

The permission model has to be designed up front, and adding a rule touches shared code rather than one component. Slower for the first rule, considerably safer by the fiftieth.

Result

Access rules live in one reviewable place, and a permission bug became a question about one module rather than a search through the whole app.

Decision 03

Cleaning user-written content once, at the edge

Problem

Posts carry rich text written by users, arriving from several places in the app. Cleaning it at the point of display meant every screen had to remember to do it, and the ones that forgot were the ones users found.

Decision

Sanitise and normalise content as it enters the application, and render it through a single trusted component.

Trade-off

Processing every response costs a pass over the data, and the original server shape is no longer available further down. Both were worth removing a whole class of security and rendering bugs.

Result

One place to reason about untrusted content, and posts that render identically in feeds, detail views and previews.

Decision 04

Organising code by feature, not by file type

Problem

A single shared components folder was already producing files nobody owned, and a change to one shared table kept breaking screens the author had never opened.

Decision

Group code by feature area, keep each area's public surface small, and only promote something to shared once a second feature genuinely needs it.

Trade-off

Some duplication survives longer than it would in a strictly DRY codebase. Duplication is cheaper than the wrong shared abstraction.

Result

Changes stayed local, and a new engineer could work inside one area without reading the whole application.
08

One permission model for four kinds of user

Most permission bugs are not logic errors. They are two places in an application disagreeing about the same question, because both were allowed to answer it.

The fix was to remove the second answer. A user's roles are resolved into a complete permission set once, when the session starts, and every feature asks the same typed question of that one result: can this person do this, here.

Resolve once
Roles across platform, district and school collapse into a single permission set at session start, rather than being re-derived per component.
One question
Features ask a single typed question. There is no second way to phrase it, so there is no second answer to drift.
Guards and controls agree
The route guard protecting a page and the button inside it read the same result, which is what stops a screen offering an action it will then refuse.
Additive, not ranked
Roles combine rather than override. Treating them as a hierarchy would have been simpler and wrong the moment someone held two at once.
Reviewable in one place
A permission change is a one-file review. That is the property that survived a year of new rules.
Typed at the boundary
The question and its answer are typed, so a rule that does not exist fails at compile time rather than silently returning false.
09

Frontend architecture

The organising question was not which components exist, but which kind of state is allowed to own which fact, because the previous structure had let three of them own the same one.

UI state

Dialogs, selection, local interaction, what is expanded

Component-local and ephemeral. It never leaves the component that owns it.

Permission state

The resolved permission set for the session

Read-only to features. Derived once, above them, so nothing below can reach a different conclusion.

Server state

Posts, users, groups, the moderation queue

RTK Query owns it outright. Nothing is copied into local state. That copy was the original bug.

Feature modules

Discussions, moderation, administration

Small public surfaces, so a change stays local and a new engineer can work in one area without reading the rest.

10

Deep dive: optimistic updates that roll back

Reactions, follows and replies have to land in a single frame. The naive version updates the screen immediately and lies whenever the request fails. The honest version needs the undo designed at the same time as the update.

User acts: reacts, follows, replies, then Cache updated immediately, then Request sent, reaching the question “Did the server agree?”. On failure: Change rolled back automatically. On success: Optimistic state kept.

UI
The interaction lands in one frame. No spinner, and no intermediate state the user has to interpret.
State
The update and its undo live together in one place, so there is no second copy of the truth that can drift from the first.
Failure
A failed request reverses itself cleanly rather than leaving the screen showing something that was never saved.
Cost
Each action has to know which parts of the cache it touches, so adding a new list means revisiting them. Real coupling, but visible and testable.
11

Trade-offs I had to make

Instant feedback vs truthful state

Updating the screen immediately is what makes a discussion feel alive, and it is also how an interface ends up showing something the server never accepted.

ChoiceOptimistic updates with automatic rollback, with the update and its undo written together. The coupling that creates is real, but it is visible and testable. The alternative was neither.

Designing the permission model up front vs adding rules as they arrive

A model designed before the rules are all known is slower for the first rule and constrains the shape of later ones.

ChoiceDesign it up front anyway. Slower for the first rule, considerably safer by the fiftieth, and it kept permission changes to a one-file review a year later.

Feature modules vs strictly shared code

Grouping by feature leaves some duplication alive longer than a strictly DRY codebase would tolerate.

ChoiceAccept the duplication and only promote to shared once a second feature genuinely needs it. Duplication is cheaper than the wrong shared abstraction.

12

Where it is now

The frontend became something a growing team could work in. Permission logic has one home, server data has one owner, and features are added inside module boundaries rather than across them.

On the product side, the discussion work shipped alongside a measured 15% increase in posts, and the performance work produced a 30% improvement in load times. The platform also contributed to a major customer renewal worth roughly $2M. That was a team outcome across product, design, backend and engineering, of which the frontend was one input.

Owned
The frontend architecture of a platform serving 500K+ educators
Built
Discussion, moderation and administration end to end
Designed
One permission layer covering four kinds of user
Improved
Load times by 30%, tracked against LCP, INP and CLS
Shipped
Discussion work that came with a measured 15% increase in posts
13

What this experience changed

Most permission bugs are two places disagreeing

Not a wrong rule, but a second place allowed to answer the same question. Removing the second answer fixed a category of bug rather than instances of it.

Copying server data into local state is the bug

It always starts as a performance shortcut and always ends as drift between the screen and the server. Giving server data exactly one owner removed the whole class.

Structure that looks like over-engineering ages well

Resolving permissions above the features looked excessive the week it landed. It is the reason permission changes were still a one-file review a year later.

Solve problems on the right side of the wire

We absorbed the complexity of page-number pagination on a live feed in the client, when it should have been fixed at the API boundary. Client-side cleverness papering over a server-side shape is a cost that compounds.

14

What I'd improve next

  • 01Extract a small typed helper for optimistic updates, since the pattern repeated itself about two actions past the point it should have been pulled out
  • 02Push earlier for cursor-based pagination on the feed, which interacts badly with page numbers and cached pages
  • 03Permission changes surfaced in the UI as an explanation, not just an absence of buttons
  • 04A moderation audit view built for the people reviewing decisions, rather than reconstructed from records
  • 05Tighter revalidation on very long sessions, where the current background refresh is more conservative than it needs to be

05 / Contact

Have a complex product problem worth solving?

Available for senior frontend, product engineering, and AI product engineering opportunities.

connection.openavailable
  1. 01
  2. 02
  3. 03

    Location

    Bangalore, India · Remote