Skip to content

About

Simple management system for users and roles

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Management System

My go-to fullstack stack for new projects. Everything here is up to date with the latest versions. The frontend is Vue but the architecture is nearly identical to React — swap Pinia for Zustand, Vue Router for React Router, composables for hooks, and you're there. TanStack Query, up-fetch with zod or valibot, and the entire backend stay the same.

Setup

docker-compose up

That's it. DB gets seeded automatically on first run.

Test Users

Email Password Role
admin@test.com password Admin
editor@test.com password Editor
viewer@test.com password Viewer

Tech Stack

  • Frontend: Vue 3 + TypeScript, Pinia, TanStack Query, up-fetch
  • Backend: NestJS + TypeScript, Drizzle ORM
  • DB: PostgreSQL
  • Auth: JWT + bcrypt
  • Validation: Zod (shared between FE/BE)
  • Linting/Formatting: Biome
  • Monorepo: Nx + pnpm

Architecture Decisions

Nx monorepo and shared types

The project uses Nx to manage the monorepo. Adds extra complexity but the main benefit here is the shared library (libs/shared/) — Nx sets up TypeScript path aliases so both frontend and backend can import from @managment-system/shared. All Zod schemas, inferred types and permission constants live there, so validation logic and types stay in sync without duplication.

Dual-layer permission enforcement

Permissions are checked on both sides. Backend uses a guard chain (middleware attaches user from JWT -> AuthGuard checks authentication -> PermissionsGuard checks required permissions). Frontend uses a usePermissions() composable that exposes hasPermission() — components use it with v-if for visibility and :disabled for greyed-out state.

up-fetch for API calls

I used up-fetch as the HTTP client on the frontend instead of axios. It's a thin wrapper around the native fetch API that adds defaults (base URL, headers, error handling) without the overhead of a full HTTP library. The API client configures it once with the JWT token injection and 401 handling, and all service files just call it directly.

Drizzle over TypeORM

Went with Drizzle ORM over TypeORM or Prisma. Simpler, more explicit, very close to raw SQL, and pairs well with Zod since I can keep everything TypeScript-native without decorators or class-transformer.

Biome over ESLint

I went with Biome instead of ESLint + Prettier. It handles both linting and formatting in one tool, also good support for monorepo, to exted base biome.json.

Tanstack Query setQueryData over invalidateQueries

Mutations use setQueryData to update the server cache directly instead of invalidateQueries. When the API already returns the updated entity, there's no reason to refetch the API — just patch the cache. It's instant (no loading flicker), saves a network request, and keeps related caches in sync (e.g. updating a role also patches that role inside the users list and current user).

Pinia with persisted state

Pinia handles state management on the frontend. For some store is uses pinia-plugin-persistedstate to automatically store state to session/local storage — similar to Zustand's persist middleware in React. You just add persist: true to the store definition and the plugin takes care.

Permission model

Permission keys use the resource:action format (e.g. users:view, roles:edit) — this is a common RBAC convention that keeps keys readable and easy to group by resource.

DB schema

Four tables: users, roles, permissions, role_permissions. Migrations run on backend startup.

About

Simple management system for users and roles

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages