Menu
Dev.to #architecture·June 7, 2026

Architectural Lessons from Next.js and Supabase Projects

This article shares seven crucial architectural decisions the author would change in Next.js and Supabase projects, highlighting common pitfalls and best practices for building scalable and maintainable applications. It covers topics from data access patterns and authentication to database connection pooling and multi-tenancy, providing practical advice to avoid costly refactorings.

Read original on Dev.to #architecture

When building applications, especially those expected to scale, early architectural decisions can significantly impact future development and maintenance. This article focuses on lessons learned from real-world Next.js and Supabase projects, emphasizing the importance of foresight in several key areas to prevent issues like `too many connections` errors, security vulnerabilities, and difficult refactoring.

Structuring Data Access and Business Logic

  1. Data Access Layer (DAL): Instead of directly calling database clients within UI components, a dedicated data access layer (e.g., `src/lib/data/posts.ts`) centralizes data operations. This simplifies adding caching, error handling, and query modifications, making the codebase more modular and maintainable.
  2. User Roles and Permissions: Storing dynamic permissions like user roles directly in JWT metadata can lead to stale permissions until token expiry. A more robust approach involves storing roles in a dedicated database table and querying it at runtime, ensuring immediate reflection of permission changes and enabling flexible Row-Level Security (RLS) policies.
  3. Business Logic vs. RLS: Row-Level Security (RLS) policies should primarily focus on access control (who can see/modify what data). Complex business logic (e.g., subscription checks for post creation) should reside in application code (Server Actions, API routes) where it can be properly tested, debugged, and provide meaningful error messages. Mixing business logic with RLS complicates both debugging and testing.

Database Best Practices and Scalability

💡

Proactive Database Connection Management

Always set up a database connection pooler (e.g., Supabase's port 6543) from day one. Using direct connections for development can lead to `too many connections` errors in production as traffic grows. Migrating to a pooler later can be time-consuming due to environment variable updates and potential driver incompatibilities.

  • Multi-Tenancy Planning: If there's any potential for team accounts or shared resources, incorporate an `org_id` column into core tables early on. Retrofitting multi-tenancy later involves significant schema migrations, query updates, and RLS policy rewrites across the entire application.

Security and Developer Experience

  • Server-Side Authentication: For server-side authentication checks, always use `getUser()` (or equivalent validated methods) rather than `getSession()`. `getSession()` reads from the cookie without validating the token with the auth server, making it vulnerable to tampered cookies. `getUser()` performs a network call to validate the JWT, ensuring secure authentication.

These architectural considerations, while sometimes seeming like overhead in early development, significantly reduce technical debt, improve scalability, and enhance security in the long run. Adopting these practices proactively can save considerable time and effort as an application matures.

Next.jsSupabaseArchitectureData Access LayerDatabase PoolingMulti-tenancyAuthenticationSecurity

Comments

Loading comments...