A comprehensive home and life management web application for tracking documents, finances, tasks, repairs, inventory, and more.
- Dashboard - Overview of balances, upcoming reminders, and recent tasks
- Bank Accounts - Track accounts with masked numbers (last 4 digits only)
- Cards - Manage cards with expiry tracking (no PIN/CCV stored)
- Documents - Upload, categorize, tag, and search files
- Tasks - One-off, seasonal, recurring, and life-event tasks
- Repairs - Track home repairs with costs and contractor info
- Properties - Real estate management with tax declarations (Danove priznanie)
- Inventory - Track owned items with warranty alerts
- Persons - Household members with life events
- Reminders - Payment due dates, expiry alerts, appointments
Homata uses a Workspace → Home hierarchy, similar to tools like Notion or ClickUp:
Workspace (e.g., "John's workspace")
├── Home: "My Apartment"
│ ├── Bank Accounts, Cards
│ ├── Documents, Tasks, Repairs
│ ├── Persons (household members)
│ └── Inventory, Properties
├── Home: "Parents' House"
│ └── (separate set of data)
└── Home: "Vacation Cabin"
└── (separate set of data)
-
Workspaces are like tenants in a multi-user SaaS. When you register, a workspace is created for you (e.g., "John's workspace"). You can invite others to your workspace.
-
Homes represent separate living situations within a workspace. For example:
- You live separately from your siblings, so each manages their own Home
- You have a primary residence and a vacation property
- You help manage your parents' household
Each Home has its own bank accounts, cards, documents, tasks, repairs, inventory, and household members. The UI provides dropdowns to switch between workspaces and homes.
- Framework: Next.js 14+ with App Router, TypeScript
- Database: PostgreSQL with Prisma ORM
- Auth: NextAuth.js (credentials + OAuth-ready)
- UI: Tailwind CSS + shadcn/ui
- i18n: next-intl (English/Slovak)
- File Storage: Local filesystem (S3-ready abstraction)
- Workflow Engine: Temporal.io for durable backend operations
- Node.js 18+
- PostgreSQL 14+
- Clone and install dependencies
git clone <repo-url>
cd homata
npm install- Configure environment
cp .env.example .env
# Edit .env with your database credentials- Start services (PostgreSQL and Temporal)
docker compose up -dThis starts:
- PostgreSQL (shared by Prisma and Temporal) - ~100-200 MB RAM
- Temporal Server
- Temporal UI (http://localhost:8080)
Note: The PostgreSQL instance hosts two separate databases:
homata- Your application data (Prisma)temporal- Temporal workflow state (auto-created by Temporal on first run)
Development Setup: For development, both databases share the same PostgreSQL instance to reduce resource usage. In production, consider separate instances for better isolation and performance.
- Run database migrations
npx prisma migrate dev --name init- Generate Prisma client
npx prisma generate- Start the Temporal worker
# In a separate terminal
npm run worker:devHomata uses Temporal.io for durable workflow execution. All API operations (auth, accounts, cards, documents) are executed as Temporal workflows, providing:
- Durability: Operations survive server restarts and failures
- Observability: View all workflows in the Temporal UI at http://localhost:8080
- Reliability: Automatic retries with configurable retry policies
- Traceability: Full history of every operation
See also: BUSINESS_LOGIC.md for detailed information about how business logic is structured (activities, workflows, operations) and TEMPORAL.md for error handling patterns.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Next.js API │────▶│ Temporal Server│────▶│ Temporal Worker│
│ (routes) │ │ (orchestration)│ │ (activities) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL (shared) │
│ ├── homata database (Prisma - application data) │
│ └── temporal database (Temporal - workflow state) │
└─────────────────────────────────────────────────────────────┘
| Workflow | Description |
|---|---|
registerUserWorkflow |
User registration with workspace creation |
listAccountsWorkflow |
List bank accounts for a user's home |
createAccountWorkflow |
Create a new bank account |
updateAccountWorkflow |
Update bank account details |
deleteAccountWorkflow |
Delete a bank account |
listCardsWorkflow |
List cards for a user's home |
createCardWorkflow |
Create a new card |
updateCardWorkflow |
Update card details |
deleteCardWorkflow |
Delete a card |
listDocumentsWorkflow |
List documents for a user's home |
createDocumentWorkflow |
Upload and create a document |
downloadDocumentWorkflow |
Download a document file |
deleteDocumentWorkflow |
Delete a document and its file |
The Temporal worker processes workflows and activities:
# Development (with auto-reload)
npm run worker:dev
# Production
npm run workerThe worker connects to Temporal Server at localhost:7233 by default. Configure with:
TEMPORAL_ADDRESS- Temporal server addressTEMPORAL_NAMESPACE- Namespace (default: "default")
Homata uses next-intl for internationalization, supporting English and Slovak.
- English (en) - Default locale
- Slovak (sk) - Secondary locale
Translation files are located in messages/:
messages/en.json- English translationsmessages/sk.json- Slovak translations
Locale configuration is in src/i18n/config.ts, which defines:
locales- Array of supported locale IDslocaleNames- Object mapping locale IDs to display names
Validation: A validation script (scripts/validate/i18n-config.ts) automatically checks that all message JSON files in messages/ are properly registered in both the locales array and localeNames object. Run npm run validate to ensure all locales are properly configured.
The project includes full TypeScript type safety for translations, ensuring:
-
Type-safe namespaces: Only valid translation namespaces can be used
const t = await getTranslations("dashboard"); // ✅ Valid const t = await getTranslations("invalid"); // ❌ TypeScript error
-
Type-safe keys: Only valid translation keys within a namespace can be used
t("title"); // ✅ Valid t("invalidKey"); // ❌ TypeScript error
-
Autocomplete support: Full IDE autocomplete for both namespaces and keys
This is configured via type augmentation in src/types/next-intl.d.ts, which:
- Infers
Localetypes fromsrc/i18n/config.ts - Infers
Messagestypes frommessages/en.json
For more details, see the next-intl TypeScript documentation.
Users can change their locale preference via the language toggle in the UI, which sets a cookie that persists across sessions. The locale is read by next-intl middleware to load the appropriate translation messages.
npm run devOpen http://localhost:3000 in your browser.
The project includes automated validation scripts to enforce code quality and consistency. Run validations manually with:
npm run validateEvery page.tsx file in src/app must declare a RouteConfig entry that matches the file's path.
Path conversion rules:
- Parenthesized directories are skipped (e.g.,
(auth),(dashboard)) - Dynamic segments are preserved (e.g.,
[workspaceId],[homeSlug])
Examples:
src/app/(auth)/login/page.tsx→ Route key:"/login"src/app/(dashboard)/[workspaceId]/[homeSlug]/documents/upload/page.tsx→ Route key:"/[workspaceId]/[homeSlug]/documents/upload"
Valid pattern:
declare module "@/routes" {
interface RouteConfig {
"/login": RouteConfigEntry<{
query?: never;
params?: never;
}>;
}
}Every page.tsx file in src/app must be either:
- A client component (has
"use client"directive at the top), OR - A server component (has
getServerSidePropsexport), OR - An async server component (has
export default async function)
Valid patterns:
// Client component
"use client";
export default function MyPage() { ... }
// Server component with getServerSideProps
export const getServerSideProps = async (context) => { ... }
export default function MyPage(props) { ... }
// Async server component (App Router)
export default async function MyPage() { ... }Every .tsx file (except src/components/ui/**) with a default export must have a corresponding Props interface or type.
Rules:
- Component name:
LoginPage→ Props interface:LoginPageProps - Supports both
interfaceandtypedefinitions - Props can be defined before or after the component
Valid patterns:
// Pattern 1: Function component
interface LoginPageProps {
// ...
}
export default function LoginPage(props: LoginPageProps) { ... }
// Pattern 2: Const arrow function
interface LoginPageProps {
// ...
}
export default const LoginPage = (props: LoginPageProps) => { ... }
// Pattern 3: Async function
interface LoginPageProps {
// ...
}
export default async function LoginPage(props: LoginPageProps) { ... }Every .tsx file (except src/components/ui/**) that contains a <form> tag must be named with the -form.tsx suffix.
Rule: Forms should be extracted into separate component files.
Invalid:
src/app/(auth)/login/page.tsx(contains form but not named-form.tsx)
Valid:
src/app/(dashboard)/[workspaceId]/[homeSlug]/finances/cards/card-form.tsxsrc/app/(dashboard)/[workspaceId]/[homeSlug]/documents/upload/upload-form.tsx
Note: This validation will flag existing files that need refactoring. Forms should be extracted into separate -form.tsx component files.
Every trpc.ts file in src/features/*/ must be imported and registered in src/lib/trpc/root.ts.
Required patterns:
- Import:
import { <featdir>Router } from "@/features/<featdir>/trpc"; - Registration:
<featdir>: <featdir>Router,in theappRouterobject
Example:
// In src/lib/trpc/root.ts
import { authRouter } from "@/features/auth/trpc";
export const appRouter = createTRPCRouter({
auth: authRouter,
});Every activities.ts file in src/features/*/ must be imported and spread in src/temporal/activities.ts.
Required patterns:
- Import:
import * as <featdir>Activities from "@/features/<featdir>/activities"; - Spread:
...<featdir>Activities,in theactivitiesobject
Example:
// In src/temporal/activities.ts
import * as authActivities from "@/features/auth/activities";
export const activities = {
...authActivities,
};Every workflows.ts file in src/features/*/ must be exported from src/temporal/workflows.ts.
Required pattern:
- Export:
export * from "@/features/<featdir>/workflows";
Example:
// In src/temporal/workflows.ts
export * from "@/features/auth/workflows";
export * from "@/features/documents/workflows";Activities (activities.ts files) can only be imported in workflows.ts or activities.ts files.
Rules:
- ✅ Activities can be imported in
workflows.tsfiles - ✅ Activities can be imported in
activities.tsfiles - ❌ Activities cannot be imported in
operations.ts,trpc.ts, or any other files - ✅ Files in
src/temporal/are exempt from this restriction
Example violations:
// ❌ Invalid: activities imported in operations.ts
import { someActivity } from "./activities";
// ❌ Invalid: activities imported in trpc.ts
import type { SomeType } from "./activities";Workflows (workflows.ts files) can only be imported in workflows.ts or operations.ts files.
Rules:
- ✅ Workflows can be imported in
workflows.tsfiles - ✅ Workflows can be imported in
operations.tsfiles - ❌ Workflows cannot be imported in
activities.tsor any other files - ✅ Files in
src/temporal/are exempt from this restriction
Example violations:
// ❌ Invalid: workflows imported in activities.ts
import { someWorkflow } from "./workflows";
// ❌ Invalid: workflows imported in trpc.ts
import type { SomeType } from "./workflows";Every .json file in messages/ must be registered in src/i18n/config.ts.
Required patterns:
- Locale in
localesarray:export const locales = [..., "<locale>"] as const; - Locale in
localeNamesobject:export const localeNames = { ..., "<locale>": "..." };
Example:
// In src/i18n/config.ts
export const locales = ["en", "sk"] as const;
export const localeNames: Record<Locale, string> = {
en: "English",
sk: "Slovenčina",
};Note: When adding a new locale file (e.g., messages/fr.json), you must:
- Add
"fr"to thelocalesarray - Add
fr: "Français"to thelocaleNamesobject
Validations run automatically before commits (if using git hooks) or can be run manually:
# Run all validations
npm run validate
# Run individual validations (via tsx)
tsx scripts/validate/app-page-route-config.ts
tsx scripts/validate/app-page-client-server.ts
tsx scripts/validate/tsx-props-interface.ts
tsx scripts/validate/tsx-form.ts
tsx scripts/validate/trpc-router.ts
tsx scripts/validate/temporal-activities.ts
tsx scripts/validate/temporal-workflows.ts
tsx scripts/validate/temporal-activities-import.ts
tsx scripts/validate/temporal-workflows-import.ts
tsx scripts/validate/i18n-config.tsIf you need to bypass validations (not recommended), you can use:
git commit --no-verifyHowever, it's better to fix the validation errors before committing.
npm run build
npm start# Open Prisma Studio (database GUI)
npx prisma studio
# Reset database
npx prisma migrate reset
# Create new migration
npx prisma migrate dev --name <migration-name>homata/
├── prisma/
│ └── schema.prisma # Database schema
├── messages/
│ ├── en.json # English translations
│ └── sk.json # Slovak translations
├── src/
│ ├── app/
│ │ ├── (auth)/ # Login, Register
│ │ ├── (dashboard)/ # Main app pages
│ │ └── api/ # API routes
│ ├── components/
│ │ ├── ui/ # shadcn components
│ │ ├── layout/ # Sidebar, Header
│ │ └── shared/ # Language toggle, etc.
│ ├── lib/
│ │ ├── auth.ts # NextAuth config
│ │ ├── prisma.ts # Database client
│ │ └── storage/ # File storage abstraction
│ └── temporal/
│ ├── client.ts # Temporal client singleton
│ ├── worker.ts # Temporal worker entry point
│ ├── activities/ # Business logic (DB, storage ops)
│ └── workflows/ # Durable workflow definitions
└── uploads/ # Local file storage
Private