A Next.js project starter with Effect-TS, designed to be cloned as the foundation for new projects.
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript 5 |
| Functional | Effect-TS |
| Database | PostgreSQL via Drizzle ORM + @effect/sql |
| Auth | better-auth (Email OTP, passwordless) |
| Resend | |
| File Storage | AWS S3 |
| Notifications | Telegram |
| Styling | Tailwind CSS 4 |
| Testing | Vitest + Playwright |
-
Clone and rename:
git clone <repo> my-project cd my-project rm -rf .git && git init git branch -M main
-
Install dependencies:
pnpm install
-
Set up environment:
cp .env.example .env.local cp .env.example .env.test # For e2e tests (use a separate test database)File Purpose .env.localDevelopment - used by Next.js, Drizzle, Vitest .env.testE2E tests - used by Playwright (separate test database) Both files are gitignored.
-
Run development server:
pnpm dev
lib/
├── core/ # Core business logic (each subfolder has own errors)
│ └── post/ # Example: getPosts()
├── services/ # Infrastructure services
│ ├── auth/ # Authentication (better-auth)
│ ├── db/ # Database (Drizzle + Effect SQL)
│ ├── email/ # Email (Resend)
│ ├── s3/ # AWS S3 file storage
│ ├── telegram/ # Telegram notifications
│ └── activity/ # Activity logging
├── layers.ts # Effect layer composition
└── next-effect/ # Next.js + Effect utilities
app/
├── (auth)/ # Auth routes (login)
├── (dashboard)/ # Protected routes
├── api/
│ ├── auth/[...all]/ # Auth API handler
│ └── example/ # Example API route
└── page.tsx # Home page example
Schema is defined in lib/services/db/schema.ts. Migrations are stored in lib/services/db/migrations/.
Use db:push for rapid iteration - applies schema changes directly without migration files:
pnpm db:pushUse db:generate to create migration files, then apply them:
pnpm db:generate # Creates migration files from schema changes
pnpm db:push # Applies migrations to database- Edit
lib/services/db/schema.ts - Run
pnpm db:generateto create migration - Review generated migration in
lib/services/db/migrations/ - Run
pnpm db:pushto apply - Commit migration files
pnpm db:studio # Opens GUI to browse/edit dataasync function Content() {
await cookies()
return await NextEffect.runPromise(
Effect.gen(function* () {
const posts = yield* getPosts()
return <div>{/* render posts */}</div>
}).pipe(
Effect.provide(Layer.mergeAll(AppLayer)),
Effect.scoped,
Effect.matchEffect({
onFailure: error =>
Match.value(error._tag).pipe(
Match.when('UnauthenticatedError', () => NextEffect.redirect('/login')),
Match.orElse(() => Effect.succeed(<ErrorPage />))
),
onSuccess: Effect.succeed
})
)
)
}const handler = Effect.gen(function* () {
const posts = yield* getPosts();
return yield* HttpServerResponse.json({ posts });
}).pipe(
Effect.catchAll(error =>
Match.value(error).pipe(
Match.tag('UnauthenticatedError', () =>
HttpServerResponse.json({ error: 'Not authenticated' }, { status: 401 })
),
Match.orElse(() =>
HttpServerResponse.json({ error: 'Internal server error' }, { status: 500 })
)
)
)
);// lib/core/example/get-something.ts
export const getSomething = (id: string) =>
Effect.gen(function* () {
const { user } = yield* getSession();
const db = yield* DbLive;
const result = yield* Effect.tryPromise(() =>
db.select().from(schema.something).where(eq(schema.something.id, id))
);
return result;
}).pipe(Effect.withSpan('example.get-something'));- Update
package.jsonname and version - Update this README
- Remove example code (
lib/core/post/, example routes) - Add your own database schema in
lib/services/db/schema.ts - Create your services in
lib/core/ - Remove unwanted services in
lib/services/. Add more services as needed (port them to the init repo).