DevCollab is a developer collaboration platform that combines GitHub-style technical content (code snippets with syntax highlighting, Markdown posts) with Discord-style workspace organization. Built as the centerpiece of my portfolio, it demonstrates production-ready full-stack engineering across seven phases of development.
Features delivered:
Developer teams share code snippets and technical knowledge in scattered tools — GitHub Gists, Notion, Slack threads — with no unified workspace and no search across all content types. The challenge: build a cohesive platform that feels native to developers while demonstrating the full breadth of senior full-stack skills in a single deployed application.
DevCollab lives inside the same Turborepo monorepo as TeamFlow, sharing infrastructure tooling while maintaining complete isolation at the application level.
apps/devcollab-web — Next.js 15 frontend with App Router and Server Componentsapps/devcollab-api — NestJS 11 with CASL deny-by-default guardpackages/devcollab-database — Isolated Prisma schema and generated clientThe Prisma client outputs to node_modules/.prisma/devcollab-client — a completely separate path from TeamFlow's @prisma/client. Both apps can coexist in the same monorepo without client collision.
| Decision | Rationale |
|---|---|
| Postgres tsvector over Meilisearch for full-text search | Zero additional Docker service. Adequate at portfolio scale. The trigger pattern (not GENERATED ALWAYS AS) eliminates Prisma migration drift — a well-known pitfall. ts_headline() provides highlighted results for free. |
| Tiptap v3 with immediatelyRender: false for Markdown editing | Tiptap v3 + Next.js 15 App Router SSR has hydration edge cases. Setting immediatelyRender: false prevents mismatches. Validated with next build + next start before merge — not just dev mode. |
| Dedicated migrate Docker service vs. migrate-on-start | Running prisma migrate deploy inside the API container causes race conditions when multiple replicas start simultaneously. A separate one-shot service with service_completed_successfully dependency guarantees exactly-once migration execution. |
| CASL deny-by-default guard as APP_GUARD | Installed before any feature controllers existed. Every new endpoint starts locked until explicitly granted. Eliminates the "forgot to add auth" class of security bugs at the architectural level. |
| Shiki server-side for published posts vs. client highlight | MarkdownRenderer is a Server Component. Shiki runs server-side via a singleton lazy-initialized highlighter. Zero client JS for code highlighting on published post views — improves performance and demonstrates SSR depth. |
Using GENERATED ALWAYS AS on Prisma schema fields causes the migration engine to regenerate the column definition on every prisma migrate dev run, producing drift warnings that would corrupt the migration history on a team.
Solution: Store the tsvector column definition in a raw trigger function (not in the Prisma schema). The trigger maintains the column; Prisma simply ignores it. GIN indexes live in manual migration SQL. Verified with a x3 migrate dev ritual — zero drift on all three runs.
Learned: Postgres-specific features (tsvector, GIN) belong in manual migration SQL when using Prisma. The Prisma schema should only contain what Prisma natively understands.
The deny-by-default CaslAuthGuard needs to extract the workspace slug from the request URL to build the correct CASL ability. Routes without a slug (like /health, /auth/login) must bypass the workspace-scoped check.
Solution: The guard extracts :slug from request.params. If slug is absent, it falls through to the service layer for authorization. Workspace-scoped routes always include the slug as the first path segment: /workspaces/:slug/snippets.
Learned: Route design choices (slug in path vs. header) have downstream effects on the entire auth architecture. Making the decision early (Phase 14) kept all subsequent controllers consistent.
Server Components cannot use credentials: 'include' for cross-origin fetches — the browser is not involved in SSR. Cookies must be forwarded manually from the incoming request to the outgoing API call.
Solution: Server Components use next/headers cookies() to read the current request cookies and forward them in the Authorization or Cookie header of the API fetch. Client Components use credentials: 'include' as usual.
Learned: SSR authentication requires explicit cookie forwarding. The Next.js 15 App Router pattern is different enough from Pages Router that it warrants its own mental model.
The demo workspace is pre-seeded with realistic content. Log in with any of three role accounts to explore the full feature set:
admin@demo.devcollab / Demo1234!contributor@demo.devcollab / Demo1234!viewer@demo.devcollab / Demo1234!