We use Postgres (or SQLite for testing/local dev). We use Drizzle ORM under the hood because it's fast and typesafe. But we wrap it in Alepha primitives to make it seamless.
Instead of writing SQL or complex class mappers, you define an $entity.
This acts as the source of truth for both your TypeScript types and your Database table structure.
1import { t } from "alepha"; 2import { $entity, db } from "alepha/orm"; 3 4// src/entities/User.ts 5export const userEntity = $entity({ 6 name: "users", // The table name 7 schema: t.object({ 8 // db.primaryKey() handles UUID/Integer/BigInt generation automatically 9 id: db.primaryKey(),10 11 // Standard TypeBox types12 email: t.email(),13 name: t.text(),14 15 // Automatic timestamp management16 createdAt: db.createdAt(),17 updatedAt: db.updatedAt(),18 }),19 // Simple index definition20 indexes: ["email"],21});
To interact with the database, you inject a repository for that entity.
1import { $repository } from "alepha/orm"; 2import { userEntity } from "./entities/User"; 3 4class UserService { 5 // This creates a type-safe repository for the userEntity 6 repo = $repository(userEntity); 7 8 async findByEmail(email: string) { 9 // .findOne, .findMany, .create, .update, .delete...10 return await this.repo.findOne({11 where: {12 email: { eq: email }13 }14 });15 }16 17 async listRecent() {18 // Pagination is built-in19 return await this.repo.paginate({20 page: 0,21 size: 20,22 sort: "-createdAt" // Descending sort23 });24 }25}
"But how do I create the table?"
Alepha integrates with Drizzle Kit. You don't need to manually write migration files for every little change during development.
alepha dev, we check your $entity definitions against the database. If they differ, we (safely) suggest or apply changes to your development DB.# Check what changed
npx alepha db:generate
# Apply changes
npx alepha db:migrate
Sometimes you need raw power.
Use the $transaction primitive to ensure atomicity.
1import { $transaction } from "alepha/orm"; 2 3class BillingService { 4 process = $transaction({ 5 handler: async (tx, userId: string, amount: number) => { 6 // Pass { tx } to repository methods to use the transaction scope 7 await this.userRepo.updateById(userId, { status: 'paid' }, { tx }); 8 await this.invoiceRepo.create({ userId, amount }, { tx }); 9 }10 });11}
If the repository helper methods aren't enough, you can drop down to raw SQL while keeping some type safety.
1import { sql } from "alepha/orm";2 3await this.repo.query(sql`4 SELECT * FROM users WHERE age > 185`);