Resolvex × Synergy Arbitration

Full-Stack Developer

July 2025 — April 2026 · 10 months
15+
Enterprise Clients
100K+
Files Managed
30%
Faster Load Times
2
Law Firm Clients
Next.jsTypeScriptNode.jsExpressPrismaPostgreSQLWasabi S3React QueryJotai
TL;DR

Built a document-heavy legal case management platform for Resolvex — an Alternative Dispute Resolution firm — handling 100K+ legal files across 10+ enterprise clients. The architecture proved robust enough that Synergy Arbitration later hired me to deploy the same system with custom branding and workflow adaptations, scaling the platform to serve 5+ additional enterprise clients.

01

The Challenge

Resolvex runs arbitration, conciliation, and mediation proceedings for enterprise clients — financial institutions, corporates, and legal entities. Each proceeding generates a rigid, sequentially ordered set of legal documents: Loan Recall Notices, Invocation Notices, Hearing Notices, Affidavits, Awards, and more.

Before this platform, the firm managed all of this over email and shared drives — a workflow that collapsed under scale. With 10+ enterprise clients each running dozens of simultaneous cases, document chaos was the core problem. Clients couldn't track the status of their cases, admins couldn't locate files quickly, and there was no audit trail of what was shared and when.

The platform needed to solve two things simultaneously: a powerful admin-side system for the firm to manage everything, and a clean, confidence-inspiring client portal where enterprise clients could track their cases without needing to call someone.

02

Architecture

I designed a clean separation-of-concerns architecture with a Next.js (App Router) frontend and a dedicated Express + Node.js backend. The two are completely decoupled — the frontend never touches the database directly. All file operations go through Wasabi Cloud Storage (an S3-compatible service), with presigned URLs generated server-side so private legal documents are never exposed publicly.

CLIENT LAYER
Admin Portal
Next.js · Full platform access
Client Portal
Next.js · Scoped to own data
NEXT.JS API ROUTES
/api/presign
S3 presigned URL generation
/api/download-file
Single file download proxy
/api/batch-download
Batch ZIP download handler
/api/preview-file
In-browser file preview
/api/upload
Secure file upload handler
EXPRESS BACKEND · NODE.JS
Auth Service
JWT · bcrypt · Role guard
Users Service
RBAC · Admin impersonation
Batches Service
Case grouping · Bulk ops
Cases Service
Status lifecycle · Filters
Files Service
Doc type classification
Analytics Service
Denormalized counters · Charts
INFRASTRUCTURE
PostgreSQL
Prisma ORM · Render.com
Wasabi S3
S3-compatible · ap-southeast-1
Vercel
Frontend hosting
Render
Backend · DB hosting

The database layer uses Prisma ORM on PostgreSQL, with a carefully designed schema that models the real-world hierarchy: a Client has Batches, each Batch has Cases, each Case has typed Files. A denormalized UserAnalytics table caches aggregate counts (total cases by status, batches by category) and is updated incrementally on every mutation — so the analytics dashboard loads instantly without expensive aggregation queries at read time.

03

Key Decisions

Several architectural decisions shaped the final system:

Express Controller — Concurrent S3 Streaming ZIP Download
// controllers/batch.controller.ts
// Streams hundreds of S3 files as a ZIP — concurrently, without buffering in memory
export const streamBatch = async (req: AuthRequest, res: Response) => {
  const CONCURRENT_DOWNLOADS = Number(process.env.FILES_DOWNLOAD_LIMIT);

  // 1. Fetch all files in the batch (cases + general files)
  const batch = await prisma.batch.findUnique({
    where: { id },
    include: { cases: { include: { files: true } }, generalFiles: true },
  });

  // 2. Create a ZIP stream piped directly to the HTTP response
  const archive = archiver('zip', { zlib: { level: 7 } });
  res.setHeader('Content-Type', 'application/zip');
  archive.pipe(res);

  // 3. Process files in concurrent batches — never all at once
  for (let i = 0; i < allFilesToZip.length; i += CONCURRENT_DOWNLOADS) {
    const chunk = allFilesToZip.slice(i, i + CONCURRENT_DOWNLOADS);

    const downloads = chunk.map(async (fileInfo) => {
      const s3Response = await s3Client.send(new GetObjectCommand({
        Bucket: process.env.WASABI_BUCKET_NAME,
        Key: fileInfo.key,
      } ));

      // Append the S3 stream to the ZIP — resolves when fully consumed
      await new Promise<void>((resolve, reject) => {
        const stream = s3Response.Body as Readable;
        stream.on('end', resolve).on('error', reject);
        archive.append(stream, { name: fileInfo.zipPath });
      });
    });

    // Wait for this chunk before starting the next — bounded concurrency
    await Promise.all(downloads);
  }

  await archive.finalize(); // Signal ZIP is complete, flush to client
};
04

The Resolvex Dashboard

The platform has two distinct portals sharing the same codebase. The Admin Portal gives the Resolvex team complete visibility — client management, batch creation, case tracking, file uploads across 12 legal document types, and an analytics dashboard with charts showing case volumes over time by category and status.

The Client Portal is deliberately stripped down — clients see only their own batches and cases, download their documents, and track where their matters stand. A role-based middleware layer on both the frontend (Next.js middleware with JWT verification via jose) and the backend (Express guards) ensures neither portal can access the other's data.

The file management system was the most complex UI surface. Each case can hold up to 12 specific document types in a fixed legal sequence. Admins upload via a multi-step modal that classifies the document type, previews it in-browser using presigned URLs, and allows batch-level ZIP downloads streamed in real time from Wasabi — no waiting for a pre-built archive.

05

Synergy Arbitration — The Adaptation

After Resolvex shipped, Synergy Arbitration — another ADR firm — engaged me to deploy the same platform for their practice. This was a meaningful validation: the architecture was solid enough to become a repeatable product, not just a one-off project.

The adaptation required more than a reskin. Synergy's arbitration workflows are more complex — I extended the document classification system from 12 to 17 legal document types, added a real-time notification systemfor case updates, and integrated Vercel Blob as a secondary storage layer. The entire custom design system was re-themed to match Synergy's brand identity, proving the value of building an abstracted component library from day one.

Combined, the platform now serves 15+ enterprise clients across two law firms, handling legal documents for arbitration and dispute resolution proceedings. The shared codebase means improvements to the core architecture benefit both deployments simultaneously.

06

Results

Across both deployments, the platform delivered measurable outcomes:

15+
Enterprise Clients
100K+
Files Managed
30%
Faster Load Times

Beyond the numbers, this project demonstrated something more important: architecture that scales to a second client is architecture worth building. By investing in a proper design system, clean API boundaries, and a well-modelled database schema from the start, the Synergy deployment took a fraction of the time of the original build — and shipped a more feature-complete product.