feat: initialize monorepo structure with Bun and Turborepo

- Add package.json for the root with workspace configuration and scripts
- Create @gruperly/config package with TypeScript configuration
- Establish shared package @gruperly/shared with Zod schemas for validation
- Implement group, student, and payment schemas using Zod
- Set up TypeScript configuration for shared package
- Document stack architecture and technical specifications in stack.md
- Configure Turborepo with build, dev, lint, and typecheck tasks
This commit is contained in:
Jose Selesan
2026-08-28 17:03:53 -03:00
parent 52b3074631
commit ed8d280995
50 changed files with 1660 additions and 1 deletions

6
apps/api/.env.example Normal file
View File

@@ -0,0 +1,6 @@
# PostgreSQL connection string
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/gruperly?schema=public"
# Better Auth
BETTER_AUTH_SECRET="replace-with-a-strong-secret"
BETTER_AUTH_URL="http://localhost:4000"

28
apps/api/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@gruperly/api",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"build": "bun build ./src/index.ts --target bun --outdir dist",
"typecheck": "tsc --noEmit",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:push": "prisma db push",
"db:studio": "prisma studio"
},
"dependencies": {
"@gruperly/shared": "workspace:*",
"@hono/zod-validator": "^0.8.0",
"better-auth": "^1.1.0",
"hono": "^4.6.0"
},
"devDependencies": {
"@types/bun": "^1.0.0",
"prisma": "^6.0.0",
"@prisma/client": "^6.0.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,171 @@
// Gruperly - Prisma Schema (PostgreSQL)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Auth (Better Auth) - required tables
model User {
id String @id @default(cuid())
name String
email String @unique
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
groupsMember Member[]
groupsOwner Group[] @relation("OwnerGroups")
waitlist WaitlistEntry[]
}
model Account {
id String @id @default(cuid())
userId String
providerId String
providerAccountId String
refreshToken String?
accessToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
idToken String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, value])
}
// Domain models
model Group {
id String @id @default(cuid())
name String
description String?
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation("OwnerGroups", fields: [createdById], references: [id], onDelete: Cascade)
members Member[]
students Student[]
payments Payment[]
}
model Member {
id String @id @default(cuid())
groupId String
userId String
role Role @default(MEMBER) // OWNER, ADMIN, MEMBER
joinedAt DateTime @default(now())
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([groupId, userId])
}
enum Role {
OWNER
ADMIN
MEMBER
}
model Student {
id String @id @default(cuid())
groupId String
fullName String
email String?
phone String?
guardianName String?
guardianPhone String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
payments Payment[]
@@index([groupId])
}
model Payment {
id String @id @default(cuid())
groupId String
studentId String
amount Decimal @db.Decimal(10, 2)
currency String @default("MXN")
status PaymentStatus @default(PENDING) // PENDING, PAID, OVERDUE, CANCELLED
dueDate DateTime
paidAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
@@index([groupId])
@@index([studentId])
}
enum PaymentStatus {
PENDING
PAID
OVERDUE
CANCELLED
}
model WaitlistEntry {
id String @id @default(cuid())
email String
name String?
status WaitlistStatus @default(PENDING) // PENDING, INVITED, JOINED, DECLINED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String? @unique
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@unique([email])
}
enum WaitlistStatus {
PENDING
INVITED
JOINED
DECLINED
}

View File

@@ -0,0 +1,3 @@
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()

3
apps/api/src/db/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { prisma } from './client'
export { prisma }

30
apps/api/src/index.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { authRoutes } from './routes/auth'
import { groupsRoutes } from './routes/groups'
import { studentsRoutes } from './routes/students'
import { paymentsRoutes } from './routes/payments'
import { waitlistRoutes } from './routes/waitlist'
const app = new Hono()
app.use('*', cors())
app.get('/', (c) => c.json({ name: 'Gruperly API', status: 'ok' }))
app.route('/auth', authRoutes)
app.route('/groups', groupsRoutes)
app.route('/students', studentsRoutes)
app.route('/payments', paymentsRoutes)
app.route('/waitlist', waitlistRoutes)
export default app
const port = Number(Bun.env.PORT ?? 4000)
Bun.serve({
port,
fetch: app.fetch,
})
console.log(`Gruperly API running on http://localhost:${port}`)

View File

@@ -0,0 +1,5 @@
import { Hono } from 'hono'
export const authRoutes = new Hono()
authRoutes.get('/session', (c) => c.json({ message: 'placeholder' }))

View File

@@ -0,0 +1,5 @@
import { Hono } from 'hono'
export const groupsRoutes = new Hono()
groupsRoutes.get('/', (c) => c.json([]))

View File

@@ -0,0 +1,5 @@
import { Hono } from 'hono'
export const paymentsRoutes = new Hono()
paymentsRoutes.get('/', (c) => c.json([]))

View File

@@ -0,0 +1,5 @@
import { Hono } from 'hono'
export const studentsRoutes = new Hono()
studentsRoutes.get('/', (c) => c.json([]))

View File

@@ -0,0 +1,5 @@
import { Hono } from 'hono'
export const waitlistRoutes = new Hono()
waitlistRoutes.get('/', (c) => c.json([]))

11
apps/api/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "@gruperly/config/tsconfig.base.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["bun"],
"paths": {
"@gruperly/shared": ["../../packages/shared/src/index.ts"]
}
},
"include": ["src/**/*.ts"]
}