Introducing the new Organizer Dashboard

Somewhere right now, a hackathon organizer is on application number 2,586 of 3,000. They’ve probably had the same tab open since morning. Open a hacker’s application, check their GitHub, look through their socials to see what they’ve built in public, read the answers they probably wrote just before the deadline. Check out the rest of their team, make the call on whether to accept them, then move on to the next application. And then the next. And the next.

Most people on Devfolio never see this. If you're a hacker, the platform is a place to find hackathons and ship projects. But behind every hackathon is a handful of organizers running it from the Organizer Dashboard, the part of Devfolio that only they log into. Every application you've ever sent was read, sorted and decided on there.

That dashboard was built in 2018 and had eight years of features piled onto it. We're rebuilding it, starting with the screen organizers spend the most time in. Here's what changed, on the surface and underneath.

The Old Organizer Dashboard

The first commit landed in June 2018. React 16 was new, hooks didn't exist, and the right way to build a dashboard was Redux, webpack and a folder called actions. So that's what it was. Eight years and over a thousand commits later, every feature Devfolio organizers have ever asked for lives in that codebase.

Working in it felt like eight years of good decisions, each made on top of the last. It started as Redux and class components talking to a REST API, with a separate Elasticsearch client for search. TypeScript and GraphQL arrived in 2021, and the new code was typed while the old code stayed as it was. React Query came a year later and sat down next to Redux rather than replacing it. By 2023 there was also a websocket for live collaboration.

That's four sources of data, feeding two different state layers, all of them still in use and still working. One reducer had grown to 1,374 lines and 44 case statements. None of it was broken. But every new thing we wanted to build had to carry eight years of decisions with it.

Starting With Review

The obvious move was a rewrite, but rebuilding the entire dashboard before shipping anything would have meant months of work that no organizer could use. Instead, we focused on the one tab organizers spend the most time in: Review.

Go to org.devfolio.co today and, at first, everything feels familiar. You land in the same Organizer Dashboard you've always used, until you open the Review tab and notice one small addition in the bottom-right corner: Switch to V2.

Click it, and you're in the new Review tab.

The interesting part is what happens underneath. Review V2 isn't a new page we added to the old codebase. It's a completely separate app with its own codebase, except both are served from the same org.devfolio.co/<hackathon-slug>/review URL.

We'll get into how that works later in the post. For now, this setup gave us something a months-long rewrite couldn't: we could rebuild the busiest part of the dashboard, put it in front of real organizers running real hackathons, and learn from it before touching anything else.

The new Review tab

Let's go back to application 2,586. Here's what that same afternoon looks like on the new Review tab.

The new tab is cleaner and easier to use. We won't go over every detail, but a few of the changes are worth pointing out.

  • Shortcuts. If you've played a PC game, you know WASD. We've borrowed a little of that muscle memory for reviewing too: A takes you to the previous application, D to the next, while Shift+A accepts, Shift+R rejects, and Shift+W waitlists. Press the same shortcut again to undo the action.
  • History. Organizer A waitlists an application, Organizer B accepts it, and Organizer C sends the acceptance email. The old dashboard kept a record of all of this too, but figuring out what happened meant reading through a fairly dense log. Now it's a clean timeline that tells you who did what and when at a glance.
  • Filters. The old filters could be surprisingly hard to reason about: filter by "Accepted", for example, and you wouldn't see someone whose acceptance mail had already gone out because "Accepted" and "Accepted with Mail Sent" were considered different filters. In V2, filters are additive. "Accepted" now means everyone who has been accepted, regardless of whether their acceptance email has been sent, and email status can be applied as a separate filter on top.
  • Search. Search now has qualifiers. Add team: or email: to your query to search within a specific field, which makes it much easier to narrow things down when you know what you're looking for.
  • Live updates. If a teammate accepts or rejects the application you currently have open, the change appears on your screen automatically, so everyone stays in sync and the same applicant doesn't accidentally get reviewed twice.
  • AI review you can tune. Instead of giving you a black-box score, AI review starts with a rubric you control: you decide how much GitHub, Devfolio history, and the application itself should count, along with what "good" looks like for each. Every application then gets a score with the reasoning behind it.

That's the part you can see. The rest of this post is about what's underneath: what a codebase built in 2026 actually looks like, and how two apps end up sharing one dashboard.

If you're here for the engineering details, keep reading. If not, feel free to skip ahead to the What's next section at the end.

What the new codebase looks like

Before we could start over, we had to figure out what we actually wanted to build it with. So, like any serious company making important technical decisions, we asked Reddit.

Jokes aside, we did get some genuinely useful input from the community. We took those suggestions back, spent a couple of days experimenting with them, and eventually settled on the stack that felt right for our use case.

A few of those choices ended up shaping the codebase quite a bit. The first wasn't even a library: it was how we decided to organize the code itself.

Feature-Sliced Design (FSD)

Feature-Sliced Design is a way of organizing frontend code into layers, with some rules around what those layers are allowed to know about each other.

The important bit in that diagram is the direction. Code can reach down the stack, but not back up it. A feature can use an entity, and an entity can use something from shared, but shared shouldn't know that the feature above it even exists.

It sounds like a fairly small rule, but it gives you a useful constraint as the codebase grows. New code usually has an obvious place to live, and dependencies have fewer chances to start spreading in every direction.

That structure has also been useful when working with agents. If an agent already knows where a feature, entity, API call, or shared utility is supposed to live, it has fewer opportunities to invent a new pattern every time it makes a change.

Managing state

Every React app eventually has to answer the question of state management. Start with useState, lift some state up, add Context, and eventually you start wondering whether it's time for Redux, Zustand, Jotai, or something else.

When we started Review V2, we assumed we'd eventually make that choice too. Before picking one, though, we looked at what actually needed to be shared.

Let's look back at the Review tab


There are stats at the top, a table of applications below them, and clicking a row opens everything about that application: the hacker, their answers, team, history, notes, and status.

Almost all of that already comes from the API. The stats, the rows, and the application you're reviewing aren't really client state at all. They're different views over data the server already owns.

Once you frame the problem that way, React Query starts looking a lot like state management for your API. Fetch something and it stores the result under a query key. Ask for that key somewhere else and you get the same cached result. Change something and you can update or invalidate the cache.

The source of truth stays on the server, while React Query handles loading, errors, caching, and refetching. So we never ended up introducing another global state store for Review.

Filters

There is still one important piece of shared state that doesn't come from the server: filters.

The server doesn't know that an organizer has decided to look at accepted applications from India with an AI score above 60. Several parts of the page need to know about that choice, so this is exactly the sort of state we could have put into a store.

Instead, we put it in the URL.

A filter is really a description of what you're looking at, and that's already what a URL is for. TanStack Router parses and validates those values for us, and those same values become part of the React Query key, so the flow from what the organizer chooses to what appears on screen is simply: URL → filters → query key → applications.

Click a filter and the URL changes, giving React Query a different key that can either be served from the cache or fetched from the server. Reload the page and the filters stay put; send the URL to another organizer and they open the same view. The URL was already a place to keep that state, so we let it be one.

Everything else

At this point, I could keep going and turn the rest of this post into a dependency list with opinions attached, but I’ll spare you that. We ended up with GraphQL subscriptions over WebSockets for live updates, Tailwind for styling, Base UI for components, TanStack Table and TanStack Virtual for the applications table, GraphQL Code Generator for typed queries, and Oxlint to keep our FSD boundaries in check.

There’s plenty more we could say about each of those choices, but that’s probably a blog post for another day.

~~

For organizers, the payoff isn't fewer reducers or one less state library. Simplifying the architecture meant we could spend less time wiring things together and more time on the parts they actually experience: live updates, smoother navigation, and a review flow that stays out of the way. Less time making the dashboard hold itself together, more time making it nicer to use.

One URL, Two Apps

Open org.devfolio.co/<hackathon-slug>/review and you get the Review tab. Click Switch to V2 and the page reloads at the exact same address, but now it's a different app answering. Nothing in the address bar changes, and nothing asks you to log in again. From the organizer's side, it just looks like the Review tab got an update.

The switch itself is just a cookie. Switch to V2 sets devfolio_feature_review=v2, Switch back removes it, and the page reloads. From there, a Cloudflare Worker sitting in front of both apps has to figure out which one should answer the request.

For pages, that's fairly straightforward. The Worker knows which routes V2 has taken over, which today is just Review. When a request for /<hackathon-slug>/review comes in with the V2 cookie, it goes to V2. Without it, the old dashboard answers as usual.

Assets make this a little more complicated.

Once the browser gets the HTML, it starts asking for JavaScript, CSS, and images. Say an organizer has switched Review to V2. Review asks for /assets/app.js with the V2 cookie attached, so that file needs to come from V2. Now they open Check-ins, which still belongs to the old dashboard. It asks for /assets/app.js too, with the exact same cookie attached, except this time the file needs to come from the old app.

From the Worker's point of view, those requests look identical. It sees /assets/app.js and the same cookie both times, with no reliable way to know which app the file belongs to.

The obvious fix is to tell the Worker where the request came from. Browsers already send a Referer header that can do exactly that.

The problem is that routing the assets correctly is only half the story. They also need to be cached correctly. If Check-ins requests /assets/app.js and Review later requests that same URL, the browser may already have /assets/app.js in its cache. As far as the cache is concerned, it's the same resource, regardless of which page originally requested it. The Referer doesn't give the two files different identities.

So we made V2's assets identify themselves. That takes one line in V2's Vite config:

export default defineConfig(({ mode }) => {
  return {
    base: "/v2/",
    // ...
  };
});

At build time, Vite prefixes V2's asset URLs with /v2/, so V2 asks for /v2/assets/app.js instead of /assets/app.js. The old dashboard continues to ask for /assets/app.js. Now the URL itself tells us which app the file belongs to, and the load balancer can route anything under /v2/ to V2.

Through all of this, the URL the organizer sees never changes. They're still on /<hackathon-slug>/review, and once V2 loads, TanStack Router reads that URL normally. So /<hackathon-slug>/review?status=accepted still describes the current view, which is why the URL-as-state approach from earlier continues to work.

Both apps also live on org.devfolio.co, so they share the same Devfolio session. Switching versions doesn't mean logging in again.

And that's basically the setup. It lets us run the old and new dashboards side by side without giving V2 a separate domain or changing the URLs organizers already use.

What's next

The Switch to V2 button is already on the Review tab for every organizer. There's no waitlist and nothing to request. You can try it today.

One thing hasn't made the move yet: bulk acceptance and rejection across a filtered set. For now, you can switch back to the old Review tab when you need it. It's also one of the next things on our list.

After that, there's plenty we want to build on top of Review V2. We want organizers to be able to split applications between themselves instead of five people working down the same list from the top. We want to show when someone else is reviewing alongside you, so collaboration feels live instead of only showing up afterwards in the history. And eventually, we want to move the rest of the Organizer Dashboard over the same way we moved Review.

The number of applications doesn’t change. An organizer might still open the Review tab to find 3,000 of them waiting.

The difference is in everything that happens after that: finding the right information faster, moving between applications without friction, staying in sync with the rest of the team, and spending less time fighting the dashboard itself.

Reviewing thousands of applications is still a lot of work. We can’t make those decisions disappear, but we can make each one a little faster, clearer, and easier.

That’s what V2 is for.