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].



