- TypeScript 95.9%
- CSS 3.5%
- JavaScript 0.2%
- Shell 0.2%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .cursor/rules | ||
| .gitea/workflows | ||
| deploy/traefik | ||
| e2e | ||
| prisma | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| README.md | ||
| tsconfig.json | ||
| VERSION | ||
| vitest.config.ts | ||
Betna (بيتنا)
Betna is a family hub PWA: one installable app that hosts a set of small family programs (polls, draws, quizzes, shopping lists, wishes, business ideas, places, and household payroll). It is Arabic-first, mobile-first, and designed for a single family on a self-hosted server.
Programs
| Program | What it does |
|---|---|
| Maashat (معاشات) | Tracks domestic workers, salary advances/gifts, and monthly salary payments. |
| Polls (تصويت) | Participant-only polls with live results, close/reopen, and likes. |
| Draws (قرعة) | Everyone adds options, marks ready, and the winner is drawn automatically or manually. |
| Contests (خمّن) | Open-text or multiple-choice family quizzes with secret answers and winner reveal. |
| Jamiya (جمعية) | Shared grocery / Shein-Temu / Amazon shopping lists with per-item images and purchased checks. |
| Fikhatri (في خاطري) | Family wish board grouped by food, outings, items, and events. |
| Biz Ideas (أفكار بزنس) | Family business idea board with threaded replies. |
| Places (أماكن) | Collaborative list of places with member-defined categories, photos, logos, and map pins. |
Cross-cutting features: per-app permissions, participant invitations, unseen badges, likes, a home activity feed, in-app notifications with per-app mutes and quiet hours, Web Push for installed devices, soft archive/restore, optional image uploads, PWA install/offline shell, and optional legacy ntfy alerts.
Stack
- Next.js 16 (App Router, webpack build) + React 19
- TypeScript (strict) + Zod
- PostgreSQL 16 + Prisma 7 (
@prisma/adapter-pg) - Auth.js / NextAuth 5 (credentials, JWT session, bcrypt password hashes)
- Tailwind CSS 4 + a custom design system in
src/app/globals.css - Serwist service worker + web app manifest
- sharp (WebP conversion, HEIC support) for uploads
- Leaflet + OpenStreetMap for place locations
- Web Push (VAPID,
web-push) + an in-app notification center - ntfy (optional legacy mirror)
- Docker multi-stage image (Node 24 alpine, Next standalone output)
Local development
Requirements: Node.js 24.18 (see .nvmrc), npm, and PostgreSQL 16 (or Docker).
npm install
cp .env.example .env
# edit .env, then create the schema:
npx prisma migrate deploy
npx prisma db seed
npm run dev
The first visit opens /setup, where you create the initial admin account.
The seed only registers the program catalog; it does not create users.
With Docker, start just the database and run the app on the host:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db
npm run dev
Environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string used by Prisma. |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
Database container credentials. |
AUTH_SECRET |
Required secret used to sign session JWTs. |
NEXTAUTH_URL |
Public app URL; must match the URL users open (HTTPS in production). |
APP_HOST / APP_PORT |
Host port binding for the app container (default 0.0.0.0:3000). |
UPLOAD_DIR |
Directory for processed uploads (default /app/data/uploads in Docker). |
NTFY_ENABLED / NTFY_URL / NTFY_TOPIC / NTFY_TOKEN |
Optional legacy ntfy notifications. |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT |
Web Push keys; generate with npx web-push generate-vapid-keys. Leave empty to disable push. |
NOTIFICATIONS_NTFY_MIRROR |
Keep mirroring events to the legacy ntfy topic (true by default). |
SEED_ADMIN_ALLOW / SEED_ADMIN_USERNAME / SEED_ADMIN_PASSWORD |
Optional test-only admin seeding; requires SEED_ADMIN_ALLOW=1. |
.env* files are git-ignored; keep production secrets on the server only.
Scripts
| Script | Purpose |
|---|---|
npm run dev |
Development server (webpack). |
npm run build / npm start |
Production build and standalone server. |
npm run lint |
ESLint. |
npm run typecheck |
TypeScript check. |
npm run test |
Vitest unit tests. |
npm run test:e2e |
Playwright end-to-end tests (expects a running seed DB). |
npm run verify |
Lint + typecheck + production build. |
npm run db:migrate / db:deploy / db:seed |
Prisma migration and seed commands. |
./scripts/release.sh <major|minor|patch> "message" |
Bump VERSION + package.json, commit, and push. |
Architecture
src/app/ Pages (server components) and API route handlers
src/components/ UI shell, feature components, and client widgets
src/lib/ Domain logic grouped by feature + shared auth/db/upload helpers
src/messages/ar.ts All Arabic UI strings (the only place Arabic belongs)
prisma/ Schema, migrations, and seed catalog
tests/ Vitest unit tests
e2e/ Playwright smoke/auth tests
- Pages are server components that render client widgets; data mutations go
through
/api/*route handlers. - Every API route authenticates and authorizes independently.
src/proxy.tsonly guards page navigation (/,/setup,/apps,/admin). - Permissions are per-program (
UserAppPermission). Admins bypass program checks. Invited participants are required for polls, draws, contests, and jamiya lists. - Content uses soft delete (
deletedAt) and archive (archivedAt). Inactive child content auto-archives lazily after 7 days; fulfilled wishes after 3 days. - Errors are stable English codes (
src/lib/error-codes.ts) translated to Arabic bytError()in the UI. - Uploads are validated, converted to WebP (max 1200px, q82), and served only to users with access to the owning program.
Notifications
Every new wish, idea, reply, shopping list/item, poll, draw, contest, place,
and like creates targeted in-app notifications for the people who should care
(app members, list participants, activity participants, or the content owner).
The bell in the top bar shows the unread count, and /apps/notifications
keeps the history.
Delivery channels:
- In-app is always on; per-app mutes hide both the in-app row and push.
- Web Push goes to devices that opted in from
حسابي → إعدادات الإشعارات. It requires theVAPID_*variables and HTTPS. Quiet hours suppress push but still keep the in-app row. - ntfy is mirrored for now (
NOTIFICATIONS_NTFY_MIRROR=true). Set it tofalseonce the family has enabled Web Push; its per-app mutes do not apply to ntfy because the legacy channel uses one shared topic.
Generate keys once with npx web-push generate-vapid-keys and keep the private
key on the server only.
Database changes
npx prisma migrate dev --name <change_name>
Migrations run automatically on container start
(scripts/docker-entrypoint.sh). Do not edit applied migrations; add a new
one instead.
Deployment
cp .env.example .env # fill in real secrets
docker compose up -d --build
The compose stack runs PostgreSQL and the app, with named volumes for the
database and uploads. The app listens on APP_PORT (3000 by default) and is
expected to sit behind a TLS reverse proxy (for example Traefik; see
deploy/traefik/). Set NEXTAUTH_URL to the public HTTPS URL; secure session
cookies will not work over plain HTTP.
Back up the postgres_data and uploads_data volumes together.
Testing and CI
npm run testruns the Vitest suite (draw logic, contest serialization, permissions, rate limiting, ntfy payloads, upload detection, relative time).npm run test:e2eruns Playwright against a built app and a seeded database..gitea/workflows/ci.ymlruns install, Prisma generate, migrations, unit tests,npm run verify, and Playwright on every push/PR tomain.
For local E2E, create a test admin first:
SEED_ADMIN_ALLOW=1 SEED_ADMIN_USERNAME=e2e SEED_ADMIN_PASSWORD=e2e-password npx prisma db seed
E2E_ADMIN_USERNAME=e2e E2E_ADMIN_PASSWORD=e2e-password npm run test:e2e
Versioning
VERSION is the source of truth and must match package.json. The running
version is shown on the home hero and the profile page, and exposed at
/api/version for monitoring. Every shipped
change goes through scripts/release.sh, which commits as
vX.Y.Z: <description> and pushes to main. CI runs on the resulting commit.
License
Private family project. All rights reserved.