A modern fullstack Next.js + Supabase application for managing Notes, Blog Posts, and User Profiles with Tags and Media Uploads.
# Install dependencies
pnpm install
# Create .env file
cp .env.example .env
# Configure environment variables inside .env
# Run development server
pnpm devSupabase CLI is required to run the database locally. You can install it using the following .
# Install Supabase CLI
brew install supabase/tap/supabase
# Upgrade (if already installed)
brew upgrade supabase
# Login
supabase login
# Press enter to open the browser and login to your Supabase account
# paste the OTP code into the terminal
# Try to run the command
pnpm gen:typesSee lib/supabase/data/README.md for database schema, triggers, policies, and seed data.
Scripts for database administration are located in lib/supabase/scripts/:
delete-user.ts— Delete a user and their related datareset-test-password.ts— Reset E2E test user password
- Next.js 16
- HeroUI
- Tailwind CSS
- Tailwind Variants
- TypeScript
- Framer Motion
- next-themes
- Supabase
- MDXEditor
- ZOD
- 🔥 Authentication (Supabase OAuth / Email)
- 📝 Create and manage Notes
- 📰 Create, edit, delete Blog Posts
- 🏷️ Tags management
- 📂 Upload and manage images
- 👤 User Profile update
- 🎨 TailwindCSS UI
- ⚡ Fast and typed APIs (TypeScript + Zod)
- 🧹 Linting (ESLint, Prettier, Husky hooks)
- Framework: Next.js 16 (App Router, Turbopack)
- UI Library: HeroUI (React components)
- Styling: Tailwind CSS v4
- Authentication: Supabase Auth (OAuth + Email)
- Database: Supabase Postgres
- Language: TypeScript (strict mode)
- Validation: Zod + ZSA (Zod Server Actions)
- Testing: Playwright (E2E)
- Code Quality: ESLint 9, Prettier, Husky
| Command | Description |
|---|---|
pnpm dev |
Start dev server with Turbopack |
pnpm build |
Production build |
pnpm start |
Start production server |
pnpm eslint |
Lint and fix code |
pnpm type:check |
Type check without emit |
pnpm type:build |
Type check with build mode (recommended) |
pnpm pretty |
Format code with Prettier |
pnpm gen:types |
Generate Supabase types |
pnpm test:e2e |
Run Playwright E2E tests |
pnpm test:e2e:ui |
Run Playwright with UI |
This project uses Playwright for end-to-end testing.
e2e/
├── .auth/ # Stored auth state
├── .results/ # Test artifacts
├── .report/ # HTML test reports
├── auth.setup.ts # Authentication setup
├── auth.spec.ts # Auth flow tests
├── blog.spec.ts # Blog feature tests
├── navigation.spec.ts
└── notes.auth.spec.ts # Tests requiring auth (*.auth.spec.ts)
# Run all tests
pnpm test:e2e
# Run with UI mode
pnpm test:e2e:ui
# Run specific test file
pnpm test:e2e e2e/blog.spec.ts- Tests requiring authentication use
*.auth.spec.tsnaming - Auth state is saved to
e2e/.auth/user.json - Tests run against
http://localhost:3000 - Dev server starts automatically
E2E_TEST_EMAIL=your_test_email@example.com
E2E_TEST_PASSWORD=your_test_passwordCode is organized by feature in /features/:
features/
├── auth/
│ ├── actions/ # Server actions
│ ├── components/ # React components
│ └── validators/ # Zod schemas
├── post/
├── note/
├── tag/
└── storage/
Server actions use ZSA for type-safe, validated server functions:
// Public action
import { baseProcedure } from '@/lib/zsa/baseProcedure';
export const getPosts = baseProcedure
.input(z.object({ limit: z.number().optional() }))
.handler(async ({ ctx, input }) => {
return ctx.supabase.from('posts').select().limit(input.limit ?? 10);
});
// Authenticated action
import { authedProcedure } from '@/lib/zsa/authedProcedure';
export const createPost = authedProcedure
.input(PostSchema)
.onSuccess(revalidatePosts)
.handler(async ({ ctx, input }) => {
return ctx.supabase.from('posts').insert(input);
});HeroUI components require client-side rendering. Import from the wrapper:
// Always use this import (not @heroui/react directly)
import { Button, Input, Modal } from '@/lib/heroui';| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_APP_NAME |
Yes | Application display name |
SUPABASE_PROJECT_ID |
Yes | Supabase project ID |
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase API URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase anonymous key |
NEXT_PUBLIC_SITE_URL |
No | Production URL for OAuth redirects |
SUPABASE_DB_PASSWORD |
No | DB password (for init scripts) |
SUPABASE_DB_REGION |
No | DB region (for init scripts) |
SUPABASE_SERVICE_ROLE_KEY |
No | Service role key (admin scripts) |
E2E_TEST_EMAIL |
No | Test user email (E2E tests) |
E2E_TEST_PASSWORD |
No | Test user password (E2E tests) |
If styles aren't working with pnpm, ensure @heroui/theme is a direct dependency:
pnpm add @heroui/themeThis creates the symlink at node_modules/@heroui/theme/ that Tailwind needs.
This project uses ESLint 9 flat config. If you see config errors:
- Ensure
eslint.config.mjsexists (not.eslintrc) - Use direct plugin imports (no
FlatCompat)
With useUnknownInCatchVariables: true, handle errors properly:
try {
// ...
} catch (e) {
const message = e instanceof Error ? e.message : 'Unknown error';
}# Kill process on port 3000
lsof -ti:3000 | xargs kill -9Husky runs these checks before each commit:
- TypeScript -
tsc -btype checking - ESLint - Lint and auto-fix staged files
- Prettier - Format staged files
- E2E Tests - Run Playwright tests
git commit --no-verify -m "emergency fix"- Use
cn()for class merging (from@/lib/utils/cn) - Prefer
tailwind-variantsfor component variants - Separate type imports:
import type { X } from 'y' - Feature-based file organization
This project was migrated from Next.js 15 to Next.js 16. Key changes:
- Middleware → Proxy: Renamed
middleware.tstoproxy.tsand exportedproxyfunction instead ofmiddleware - Typed Routes: Updated
PagePropsto use Next.js 16 typed routes syntax (PageProps<'/blog/[slug]'>instead of custom generic) - Client Component Boundaries: Created
LinkComponentwrapper for passing Next.jsLinkto HeroUI'sasprop (functions can't be passed directly to Client Components in Next.js 16)
.
├── app/ # Next.js App Router (pages, layouts, API routes)
│ ├── (private)/ # Authenticated routes (notes, profile, blog management)
│ ├── api/ # API endpoints (auth callbacks, uploads)
│ └── blog/ # Public blog routes
├── components/
│ ├── guards/ # Auth guards (OnlyAuth, OnlyRole)
│ ├── icons/ # Icon components
│ ├── providers/ # React context providers
│ └── ui/ # Reusable UI components (form, layout, etc.)
├── config/ # App configuration (fonts, site metadata)
├── e2e/ # Playwright E2E tests
│ ├── .auth/ # Stored auth state
│ ├── .results/ # Test artifacts
│ └── .report/ # HTML reports
├── features/ # Feature-based modules
│ ├── auth/ # Authentication (login, OAuth)
│ ├── note/ # Notes CRUD
│ ├── post/ # Blog posts CRUD
│ ├── storage/ # File uploads
│ ├── tag/ # Tags management
│ └── user/ # User profiles
├── lib/ # Shared utilities
│ ├── heroui/ # HeroUI client wrapper
│ ├── next/ # Next.js helpers (metadata)
│ ├── rbac/ # Role-based access control
│ ├── supabase/ # Supabase client & helpers
│ ├── utils/ # General utilities (cn, etc.)
│ └── zsa/ # ZSA procedures (base, authed)
├── public/ # Static assets
├── styles/ # Global CSS, Tailwind config
├── supabase/ # Supabase migrations & config
└── types/ # TypeScript type definitions
Use this route group as a way to group routes that are only accessible to authenticated users.
- login
- /blog
- /create
- /edit/[id]
- /notes
- profile
- tags
[GET]/api/auth/callback– callback route for the OAuth providers;[GET]/api/auth/confirm– confirm the email OTP and redirect the user to the next page;[DELETE]/api/posts– delete a blog post by id;[DELETE]/api/notes– delete a note by id;[POST]/upload– upload an image to the supabase storage;
- /blog/[slug] – view a blog post by slug;
- /blog – view all blog posts;
In the application, interaction with the server-side can be managed through two primary methods: server-actions and browser requests to API
endpoints. Each method leverages the entityService as an entry point, ensuring consistency in how data is managed and operations are
performed.
Server-actions are methods defined on the server that directly handle the lifecycle of requests—from parsing data to calling service methods and formatting responses. This approach is considered the default and recommended for its ability to tightly integrate with server logic and services.
Example Usage:
import { postCreate, postUpdate } from '@/server/actions/post';
const DummyExample = () => {
const [response, formAction] = useFormState(action, { statusText: '', status: 0, data: null });
return (
<Form action={formAction}>
<Submit />
</Form>
);
};- Direct access to server resources and services.
- Efficient handling of data validation and transformation.
- Consolidated error handling and response formatting.
Browser requests are a simpler and more straightforward method where the frontend sends HTTP requests directly to defined API endpoints. These endpoints parse the requests, perform operations via the entity services, and send back the responses.
Example Usage:
import { useApi } from '@/hooks/useApi';
export const DeletePostButton = ({ id }: TPostId) => {
const [deletePost, pending] = useApi<TPostId>('delete', 'posts');
return (
<Button isLoading={pending} onClick={() => deletePost({ id })}>
<Trash />
</Button>
);
};- Less code, ideal for simple CRUD operations.
- Supports all HTTP methods.
- Manages request states using React's useTransition for smooth user experiences.
- Automatically refreshes components or pages upon request completion.
Both methods, server-actions and browser requests, are effective for interacting with the server-side but cater to different needs and complexities in application architecture. Server-actions offer more robust handling at the cost of tighter coupling, while browser requests provide flexibility and simplicity, ideal for scenarios where rapid development and deployment are prioritized.
Made with ❤️ by [floatrx].



