Skip to content
Moeijiro
← All projects

Case study · Client portal · full-stack

ForgeDesk

Client portal for freelancers and studios: projects, tasks, versioned deliverables and client approvals.

Two roles in one app: clients see only the projects they're invited to.

Full-Stack Apps
Type
Personal open-source project
Automated tests
43 (pytest)
Built with
Python · FastAPI · SQLAlchemy
Live demo
Runs locally — not hosted
Client portal: what needs a decision, projects and latest updates

Overview

What it does

ForgeDesk gives each client one calm place to follow a project — progress, deliverables to approve, shared files and short updates — while the studio runs tasks, reviews and deadlines for every client from one dashboard. It deliberately covers one use case, studio ↔ client collaboration: no workflow engine, custom fields or Gantt chart.

Problem

Freelancers juggle email threads, file links and “which version did you approve?”. A portal only helps if clients can't see each other's data and approvals are recorded against a specific version.

Solution

Two roles with one access layer: every project query goes through accessible_projects(), so a client user only ever sees their own client's projects. Deliverables are versioned; a new version returns to review, and approvals or change requests are tied to it.

Screenshots

From the running application

  • Client portal: what needs a decision, projects and latest updates
    Client portal: what needs a decision, projects and latest updates
  • Deliverables with version history and review status
    Deliverables with version history and review status
  • Studio dashboard with projects, reviews and deadlines
    Studio dashboard with projects, reviews and deadlines
  • Task board
    Task board
  • Single-use client invitation
    Single-use client invitation
  • Client portal on a phone
    Client portal on a phone

Captured from the app running locally with its seeded demo data. Select an image to open it full size.

Architecture

How the pieces fit

Studio app/appClient portal/portalNext.jsApp Router · React 19AuthJWT HttpOnly cookieAccess layeraccessible_projects()Routes & servicesactivity log · uploadsDatabaseSQLAlchemy 2File storageStorage protocol
  • Studio app → Next.js
  • Client portal → Next.js
  • Next.js → Auth
  • Auth → Access layer
  • Access layer → Routes & services
  • Routes & services → Database
  • Routes & services → File storage
  • Core service
  • External system
  • Storage / entry point

Core features

What's in it

  • Studio dashboard

    Active projects, pending reviews, overdue tasks, deadlines in the next three weeks and recent activity from live queries.

  • Versioned deliverables

    Uploads with version history; a new version goes back to the client, older versions stay downloadable.

  • Client approvals

    Approve or request changes with a written note the studio sees.

  • Single-use invitations

    Client users exist only through invitations stored as SHA-256 digests; access can be removed anytime.

  • Files & updates

    Shared files both ways through a four-method storage protocol, and progress notes on the client's timeline.

  • Every state designed

    Light and dark themes, mobile layouts, and loading, empty and error states on every page.

Technical decisions

Interesting problems, and how they're solved

  1. 01

    One access layer

    Authorization is a query, not an afterthought: resources outside the user's scope return 404, never a 403 that confirms they exist.

  2. 02

    Storage behind a protocol

    Local disk today; the four-method Storage protocol is the seam for S3-compatible storage.

  3. 03

    Strict inputs

    Pydantic models reject unknown fields and trim strings; timestamps are UTC and always returned with an offset.

backend/app/api/deps.py · L63–76View on GitHub ↗
def accessible_projects(user: User) -> Select[tuple[Project]]:
    query = select(Project)
    if user.role == Role.OWNER:
        return query.where(Project.owner_id == user.id)
    if user.client_id is None:
        return query.where(false())  # a client user without a client sees nothing
    return query.where(Project.client_id == user.client_id)


def load_project(db: Session, user: User, project_id: int) -> Project:
    project = db.execute(accessible_projects(user).where(Project.id == project_id)).scalar_one_or_none()
    if project is None:
        raise not_found("Project")
    return project

Stack & skills

Built with

Technology

  • Python
  • FastAPI
  • SQLAlchemy
  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind CSS
  • shadcn/ui
  • Motion

What this project demonstrates

  • Full-stack product
  • Role-based access
  • File handling
  • Product UX
  • Next.js 16 + FastAPI

Repository & demo

Run it yourself

The seed creates a studio owner and client users for three fictional companies — logins are listed in the README.

No public live demo is hosted. Everything below runs locally with Python 3.12+ and Node 20+.

$ git clone https://github.com/Moeijiro/forgedesk.git
$ cd forgedesk
$ make install
$ make seed
$ make api
$ make web

Have a project in mind?

I build APIs, automation systems, Discord integrations and full-stack applications. Tell me what it should do and I'll tell you how I'd build it.