feat: initial backend setup with Bun, Hono, and Drizzle ORM

- Added documentation for architecture and code plan
- Implemented database schema for users, expenses, settlements, and transactions
- Set up double-entry bookkeeping system with multi-currency support
- Configured Hono web framework with middleware
- Integrated Drizzle ORM for database operations
This commit is contained in:
Yadunand Prem 2025-05-27 17:38:50 -04:00
parent de91da311f
commit 74b7ddf3d6
No known key found for this signature in database
20 changed files with 1718 additions and 0 deletions

34
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules/
# TypeScript cache
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build output
dist/
build/
# Coverage directory
coverage/
# Mac files
.DS_Store
# Temporary files
*.tmp
*.temp
# Bun
.bun

15
backend/README.md Normal file
View File

@ -0,0 +1,15 @@
# backend
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.10. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

341
backend/bun.lock Normal file
View File

@ -0,0 +1,341 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "backend",
"dependencies": {
"better-auth": "latest",
"drizzle-orm": "^0.30.5",
"hono": "^4.2.4",
"pg": "^8.11.3",
"postgres": "^3.4.3",
"zod": "^3.22.4",
},
"devDependencies": {
"@types/bun": "latest",
"@types/pg": "^8.10.9",
"drizzle-kit": "^0.20.14",
"typescript": "^5.3.3",
},
},
},
"packages": {
"@better-auth/utils": ["@better-auth/utils@0.2.5", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
"@hono/node-server": ["@hono/node-server@1.14.3", "", { "peerDependencies": { "hono": "^4" } }, "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w=="],
"@hono/zod-validator": ["@hono/zod-validator@0.2.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-dSDxaPV70Py8wuIU2QNpoVEIOSzSXZ/6/B/h4xA7eOMz7+AarKTSGV8E6QwrdcCbBLkpqfJ4Q2TmBO0eP1tCBQ=="],
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="],
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="],
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="],
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.0", "", {}, "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="],
"@simplewebauthn/server": ["@simplewebauthn/server@13.1.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA=="],
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
"@types/pg": ["@types/pg@8.15.2", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg=="],
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"better-auth": ["better-auth@1.2.8", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.28.1", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-y8ry7ZW3/3ZIr82Eo1zUDtMzdoQlFnwNuZ0+b0RxoNZgqmvgTIc/0tCDC7NDJerqSu4UCzer0dvYxBsv3WMIGg=="],
"better-call": ["better-call@1.0.9", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g=="],
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
"camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"cli-color": ["cli-color@2.0.4", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.64", "es6-iterator": "^2.0.3", "memoizee": "^0.4.15", "timers-ext": "^0.1.7" } }, "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA=="],
"commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
"d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"difflib": ["difflib@0.2.4", "", { "dependencies": { "heap": ">= 0.2.0" } }, "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w=="],
"dreamopt": ["dreamopt@0.8.0", "", { "dependencies": { "wordwrap": ">=0.0.2" } }, "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg=="],
"drizzle-kit": ["drizzle-kit@0.20.18", "", { "dependencies": { "@esbuild-kit/esm-loader": "^2.5.5", "@hono/node-server": "^1.9.0", "@hono/zod-validator": "^0.2.0", "camelcase": "^7.0.1", "chalk": "^5.2.0", "commander": "^9.4.1", "env-paths": "^3.0.0", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "glob": "^8.1.0", "hanji": "^0.0.5", "hono": "^4.1.4", "json-diff": "0.9.0", "minimatch": "^7.4.3", "semver": "^7.5.4", "superjson": "^2.2.1", "zod": "^3.20.2" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-fLTwcnLqtBxGd+51H/dEm9TC0FW6+cIX/RVPyNcitBO77X9+nkogEfMAJebpd/8Yl4KucmePHRYRWWvUlW0rqg=="],
"drizzle-orm": ["drizzle-orm@0.30.10", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@electric-sql/pglite": ">=0.1.1", "@libsql/client": "*", "@neondatabase/serverless": ">=0.1", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=13.2.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-IRy/QmMWw9lAQHpwbUh1b8fcn27S/a9zMIzqea1WNOxK9/4EB8gIo+FZWLiPXzl2n9ixGSv8BhsLZiOppWEwBw=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="],
"es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="],
"es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="],
"es6-weak-map": ["es6-weak-map@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.46", "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.1" } }, "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA=="],
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="],
"event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="],
"ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"hanji": ["hanji@0.0.5", "", { "dependencies": { "lodash.throttle": "^4.1.1", "sisteransi": "^1.0.5" } }, "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw=="],
"heap": ["heap@0.2.7", "", {}, "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="],
"hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="],
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
"json-diff": ["json-diff@0.9.0", "", { "dependencies": { "cli-color": "^2.0.0", "difflib": "~0.2.1", "dreamopt": "~0.8.0" }, "bin": { "json-diff": "bin/json-diff.js" } }, "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ=="],
"kysely": ["kysely@0.28.2", "", {}, "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="],
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
"lru-queue": ["lru-queue@0.1.0", "", { "dependencies": { "es5-ext": "~0.10.2" } }, "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ=="],
"memoizee": ["memoizee@0.4.17", "", { "dependencies": { "d": "^1.0.2", "es5-ext": "^0.10.64", "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", "is-promise": "^2.2.2", "lru-queue": "^0.1.0", "next-tick": "^1.1.0", "timers-ext": "^0.1.7" } }, "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA=="],
"minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"pg": ["pg@8.16.0", "", { "dependencies": { "pg-connection-string": "^2.9.0", "pg-pool": "^3.10.0", "pg-protocol": "^1.10.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg=="],
"pg-cloudflare": ["pg-cloudflare@1.2.5", "", {}, "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg=="],
"pg-connection-string": ["pg-connection-string@2.9.0", "", {}, "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
"pg-pool": ["pg-pool@3.10.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA=="],
"pg-protocol": ["pg-protocol@1.10.0", "", {}, "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="],
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="],
"timers-ext": ["timers-ext@0.1.8", "", { "dependencies": { "es5-ext": "^0.10.64", "next-tick": "^1.1.0" } }, "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"zod": ["zod@3.25.30", "", {}, "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
}
}

12
backend/drizzle.config.ts Normal file
View File

@ -0,0 +1,12 @@
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema/*',
out: './src/db/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/splitwise',
},
verbose: true,
strict: true,
} satisfies Config;

1
backend/index.ts Normal file
View File

@ -0,0 +1 @@
console.log("Hello via Bun!");

27
backend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "splitwise-backend",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "bun run src/db/migrate.ts",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"better-auth": "latest",
"drizzle-orm": "^0.30.5",
"hono": "^4.2.4",
"pg": "^8.11.3",
"postgres": "^3.4.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/bun": "latest",
"@types/pg": "^8.10.9",
"drizzle-kit": "^0.20.14",
"typescript": "^5.3.3"
}
}

24
backend/src/db/index.ts Normal file
View File

@ -0,0 +1,24 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
// Get database connection string from environment variable or use default
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/splitwise';
// Create a PostgreSQL client
const client = postgres(connectionString, { max: 10 });
// Create a Drizzle ORM instance
export const db = drizzle(client);
// Function to run migrations
export async function runMigrations() {
try {
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './src/db/migrations' });
console.log('Migrations completed successfully');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
}

14
backend/src/db/migrate.ts Normal file
View File

@ -0,0 +1,14 @@
import { runMigrations } from './index';
// This file is used to run migrations from the command line
async function main() {
try {
await runMigrations();
process.exit(0);
} catch (error) {
console.error('Migration script failed:', error);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,39 @@
import { pgTable, varchar, integer, boolean, uuid, timestamp, primaryKey } from 'drizzle-orm/pg-core';
import { users } from './users';
// Currencies table
export const currencies = pgTable('currencies', {
code: varchar('code', { length: 3 }).primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
symbol: varchar('symbol', { length: 10 }).notNull(),
decimalDigits: integer('decimal_digits').notNull().default(2),
isActive: boolean('is_active').notNull().default(true),
});
// Exchange Rates table
export const exchangeRates = pgTable('exchange_rates', {
id: uuid('id').primaryKey().defaultRandom(),
fromCurrencyCode: varchar('from_currency_code', { length: 3 }).notNull().references(() => currencies.code),
toCurrencyCode: varchar('to_currency_code', { length: 3 }).notNull().references(() => currencies.code),
rate: varchar('rate', { length: 24 }).notNull(), // Using varchar for high precision decimal
effectiveDate: timestamp('effective_date', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
currencyPairDateIdx: primaryKey({ columns: [table.fromCurrencyCode, table.toCurrencyCode, table.effectiveDate] }),
};
});
// Accounts table (for double-entry bookkeeping)
export const accounts = pgTable('accounts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: varchar('type', { length: 20 }).notNull(), // 'asset' or 'liability'
currencyCode: varchar('currency_code', { length: 3 }).notNull().references(() => currencies.code),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
userCurrencyUniqueIdx: primaryKey({ columns: [table.userId, table.currencyCode] }),
};
});

View File

@ -0,0 +1,33 @@
import { pgTable, uuid, varchar, text, timestamp, decimal, primaryKey } from 'drizzle-orm/pg-core';
import { users } from './users';
import { currencies } from './currencies';
// Expenses table
export const expenses = pgTable('expenses', {
id: uuid('id').primaryKey().defaultRandom(),
title: varchar('title', { length: 255 }).notNull(),
description: text('description'),
amount: decimal('amount', { precision: 20, scale: 2 }).notNull(),
currencyCode: varchar('currency_code', { length: 3 }).notNull().references(() => currencies.code),
date: timestamp('date', { withTimezone: true }).notNull(),
payerId: uuid('payer_id').notNull().references(() => users.id),
receiptImageUrl: text('receipt_image_url'),
createdById: uuid('created_by').notNull().references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
// Expense Participants table
export const expenseParticipants = pgTable('expense_participants', {
id: uuid('id').primaryKey().defaultRandom(),
expenseId: uuid('expense_id').notNull().references(() => expenses.id, { onDelete: 'cascade' }),
userId: uuid('user_id').notNull().references(() => users.id),
shareAmount: decimal('share_amount', { precision: 20, scale: 2 }).notNull(),
sharePercentage: decimal('share_percentage', { precision: 5, scale: 2 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
expenseUserUniqueIdx: primaryKey({ columns: [table.expenseId, table.userId] }),
};
});

View File

@ -0,0 +1,7 @@
// Export all schema definitions
export * from './users';
export * from './currencies';
export * from './expenses';
export * from './settlements';
export * from './transactions';
export * from './notifications';

View File

@ -0,0 +1,12 @@
import { pgTable, uuid, varchar, boolean, timestamp, jsonb } from 'drizzle-orm/pg-core';
import { users } from './users';
// Notifications table
export const notifications = pgTable('notifications', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: varchar('type', { length: 50 }).notNull(),
content: jsonb('content').notNull(),
isRead: boolean('is_read').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});

View File

@ -0,0 +1,16 @@
import { pgTable, uuid, varchar, text, timestamp, decimal } from 'drizzle-orm/pg-core';
import { users } from './users';
import { currencies } from './currencies';
// Settlements table
export const settlements = pgTable('settlements', {
id: uuid('id').primaryKey().defaultRandom(),
payerId: uuid('payer_id').notNull().references(() => users.id),
receiverId: uuid('receiver_id').notNull().references(() => users.id),
amount: decimal('amount', { precision: 20, scale: 2 }).notNull(),
currencyCode: varchar('currency_code', { length: 3 }).notNull().references(() => currencies.code),
date: timestamp('date', { withTimezone: true }).notNull(),
memo: text('memo'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});

View File

@ -0,0 +1,22 @@
import { pgTable, uuid, varchar, text, timestamp, decimal } from 'drizzle-orm/pg-core';
import { expenses } from './expenses';
import { settlements } from './settlements';
import { accounts } from './currencies';
import { currencies } from './currencies';
// Transactions table (double-entry bookkeeping)
export const transactions = pgTable('transactions', {
id: uuid('id').primaryKey().defaultRandom(),
expenseId: uuid('expense_id').references(() => expenses.id, { onDelete: 'set null' }),
settlementId: uuid('settlement_id').references(() => settlements.id, { onDelete: 'set null' }),
fromAccountId: uuid('from_account_id').notNull().references(() => accounts.id),
toAccountId: uuid('to_account_id').notNull().references(() => accounts.id),
amount: decimal('amount', { precision: 20, scale: 2 }).notNull(),
currencyCode: varchar('currency_code', { length: 3 }).notNull().references(() => currencies.code),
exchangeRate: decimal('exchange_rate', { precision: 24, scale: 12 }).notNull().default('1'),
date: timestamp('date', { withTimezone: true }).notNull(),
type: varchar('type', { length: 20 }).notNull(), // 'expense' or 'settlement'
memo: text('memo'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});

View File

@ -0,0 +1,46 @@
import { pgTable, uuid, varchar, text, timestamp, primaryKey } from 'drizzle-orm/pg-core';
// Users table
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
displayName: varchar('display_name', { length: 255 }).notNull(),
profilePhotoUrl: text('profile_photo_url'),
defaultCurrencyCode: varchar('default_currency_code', { length: 3 }).notNull().default('USD'),
locale: varchar('locale', { length: 10 }).notNull().default('en-US'),
timezone: varchar('timezone', { length: 50 }).notNull().default('UTC'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
// Auth table (managed by better-auth)
export const auth = pgTable('auth', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
provider: varchar('provider', { length: 50 }).notNull(),
providerUserId: varchar('provider_user_id', { length: 255 }),
email: varchar('email', { length: 255 }).notNull(),
passwordHash: text('password_hash'),
refreshToken: text('refresh_token'),
lastLogin: timestamp('last_login', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
providerUniqueIdx: primaryKey({ columns: [table.provider, table.providerUserId] }),
};
});
// Contacts table
export const contacts = pgTable('contacts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
status: varchar('status', { length: 20 }).notNull().default('pending'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
userContactUniqueIdx: primaryKey({ columns: [table.userId, table.contactId] }),
};
});

80
backend/src/index.ts Normal file
View File

@ -0,0 +1,80 @@
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import { prettyJSON } from 'hono/pretty-json';
import { db, runMigrations } from './db';
// Import API routes (will be created later)
// import { authRoutes } from './controllers/auth';
// import { userRoutes } from './controllers/user';
// import { expenseRoutes } from './controllers/expense';
// Define port from environment or default
const PORT = process.env.PORT || 3000;
// Create a new Hono app
const app = new Hono();
// Initialize the database
const initDb = async () => {
try {
await runMigrations();
console.log('Database initialized successfully');
} catch (error) {
console.error('Failed to initialize database:', error);
process.exit(1);
}
};
// Apply middleware
app.use('*', logger());
app.use('*', cors());
app.use('*', secureHeaders());
app.use('*', prettyJSON());
// Add db to context
app.use('*', async (c, next) => {
c.set('db', db);
await next();
});
// Health check route
app.get('/', (c) => {
return c.json({
status: 'ok',
message: 'Splitwise API is running',
version: '1.0.0',
});
});
// Mount API routes
// app.route('/api/auth', authRoutes);
// app.route('/api/users', userRoutes);
// app.route('/api/expenses', expenseRoutes);
// Error handling
app.onError((err, c) => {
console.error(`Error: ${err.message}`);
return c.json({
status: 'error',
message: err.message,
}, 500);
});
// Not found handling
app.notFound((c) => {
return c.json({
status: 'error',
message: 'Not Found',
}, 404);
});
// Initialize database and start the server
initDb().catch(console.error);
console.log(`Server is running on port ${PORT}`);
export default {
port: PORT,
fetch: app.fetch,
};

38
backend/tsconfig.json Normal file
View File

@ -0,0 +1,38 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "preserve",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Path aliases
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
// Types
"types": ["bun-types"]
},
"include": ["src/**/*"]
}

585
docs/architecture.md Normal file
View File

@ -0,0 +1,585 @@
# Splitwise Architecture
This document outlines the detailed architecture for the Splitwise-style application, focusing on the database design and backend endpoints. The application implements a double-entry bookkeeping system and supports multiple currencies.
## Database Architecture
### Core Entities and Relationships
```
[Users] 1--N [Auth]
1
|
N
[Contacts]
|
N
[Users] 1--N [Accounts] N--1 [Currencies]
1 1 1 1
| | | |
| | | N
N | N |
[Expenses] | [Transactions]-+
1 | N N
| | | |
N | | |
[ExpenseParticipants] |
|
[Settlements]
|
N
[Notifications]
```
Entity Relationship Legend:
- `1` and `N` indicate one-to-many relationships
- Lines connect related entities
- Entities in square brackets `[]`
### Schema Details
#### Users
```sql
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
profile_photo_url TEXT,
default_currency_code CHAR(3) NOT NULL DEFAULT 'USD',
locale VARCHAR(10) NOT NULL DEFAULT 'en-US',
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
```
#### Auth (managed by better-auth)
```sql
CREATE TABLE auth (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_user_id VARCHAR(255),
email VARCHAR(255) NOT NULL,
password_hash TEXT,
refresh_token TEXT,
last_login TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(provider, provider_user_id)
);
```
#### Contacts
```sql
CREATE TABLE contacts (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'accepted', 'declined')),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(user_id, contact_id)
);
```
#### Currencies
```sql
CREATE TABLE currencies (
code CHAR(3) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
symbol VARCHAR(10) NOT NULL,
decimal_digits INT NOT NULL DEFAULT 2,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
```
#### Exchange Rates
```sql
CREATE TABLE exchange_rates (
id UUID PRIMARY KEY,
from_currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
to_currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
rate DECIMAL(24, 12) NOT NULL,
effective_date DATE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(from_currency_code, to_currency_code, effective_date)
);
```
#### Accounts (for double-entry bookkeeping)
```sql
CREATE TABLE accounts (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL CHECK (type IN ('asset', 'liability')),
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(user_id, currency_code)
);
```
#### Expenses
```sql
CREATE TABLE expenses (
id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0),
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
date TIMESTAMP WITH TIME ZONE NOT NULL,
payer_id UUID NOT NULL REFERENCES users(id),
receipt_image_url TEXT,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
```
#### Expense Participants
```sql
CREATE TABLE expense_participants (
id UUID PRIMARY KEY,
expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
share_amount DECIMAL(20, 2) NOT NULL CHECK (share_amount >= 0),
share_percentage DECIMAL(5, 2) GENERATED ALWAYS AS (
CASE
WHEN (SELECT amount FROM expenses WHERE id = expense_id) > 0
THEN (share_amount / (SELECT amount FROM expenses WHERE id = expense_id)) * 100
ELSE 0
END
) STORED,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(expense_id, user_id)
);
```
#### Transactions (double-entry bookkeeping)
```sql
CREATE TABLE transactions (
id UUID PRIMARY KEY,
expense_id UUID REFERENCES expenses(id) ON DELETE SET NULL,
settlement_id UUID REFERENCES settlements(id) ON DELETE SET NULL,
from_account_id UUID NOT NULL REFERENCES accounts(id),
to_account_id UUID NOT NULL REFERENCES accounts(id),
amount DECIMAL(20, 2) NOT NULL,
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
exchange_rate DECIMAL(24, 12) NOT NULL DEFAULT 1.0,
native_amount DECIMAL(20, 2) GENERATED ALWAYS AS (amount * exchange_rate) STORED,
date TIMESTAMP WITH TIME ZONE NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('expense', 'settlement')),
memo TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CHECK (expense_id IS NOT NULL OR settlement_id IS NOT NULL),
CHECK (from_account_id != to_account_id)
);
```
#### Settlements
```sql
CREATE TABLE settlements (
id UUID PRIMARY KEY,
payer_id UUID NOT NULL REFERENCES users(id),
receiver_id UUID NOT NULL REFERENCES users(id),
amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0),
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
date TIMESTAMP WITH TIME ZONE NOT NULL,
memo TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CHECK (payer_id != receiver_id)
);
```
#### Balances (materialized view or calculated on-the-fly)
```sql
CREATE VIEW user_balances AS
SELECT
u1.id AS user_id,
u2.id AS other_user_id,
t.currency_code,
SUM(
CASE
WHEN t.from_account_id = a1.id THEN -t.amount
WHEN t.to_account_id = a1.id THEN t.amount
ELSE 0
END
) AS balance
FROM
users u1
CROSS JOIN users u2
JOIN accounts a1 ON u1.id = a1.user_id
JOIN accounts a2 ON u2.id = a2.user_id
JOIN transactions t ON
(t.from_account_id = a1.id AND t.to_account_id = a2.id) OR
(t.from_account_id = a2.id AND t.to_account_id = a1.id)
WHERE
u1.id != u2.id
AND a1.currency_code = a2.currency_code
AND a1.currency_code = t.currency_code
GROUP BY
u1.id, u2.id, t.currency_code;
```
#### Notifications
```sql
CREATE TABLE notifications (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL CHECK (type IN ('expense_added', 'expense_edited', 'expense_deleted', 'settlement_received')),
content JSONB NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
```
### Double-Entry Bookkeeping Implementation
Every financial event in the system (expenses and settlements) is recorded as a transaction with two sides:
1. A debit to one account (increasing an asset or decreasing a liability)
2. A credit to another account (decreasing an asset or increasing a liability)
#### Example: Creating an Expense
When User A records a $30 expense that they paid for, which is split equally between User A, User B, and User C:
1. User A paid $30 (creates a transaction)
- Credit User A's account (they're owed money) +$20
- Debit User B's account (they owe money) -$10
- Debit User C's account (they owe money) -$10
2. Corresponding transactions are created:
- Transaction 1: from User B's account to User A's account for $10
- Transaction 2: from User C's account to User A's account for $10
#### Example: Settling a Debt
When User B settles their $10 debt to User A:
1. A settlement record is created
2. A transaction is recorded:
- Debit User A's account -$10 (they received money)
- Credit User B's account +$10 (they paid money)
### Multi-Currency Support
The system supports multiple currencies through:
1. A currencies table with ISO currency codes
2. Exchange rates tracking between currency pairs
3. Storing all transactions in their original currency
4. Recording exchange rates used at the time of transaction
5. Converting balances to user's preferred currency on-the-fly
When an expense is created in a different currency:
1. The expense is recorded in its original currency
2. Each participant's share is calculated in the original currency
3. Transactions are created in the original currency
4. Exchange rates are applied when viewing balances in a different currency
## Backend API Endpoints
### Authentication (managed by better-auth)
```
POST /api/auth/register
POST /api/auth/login
POST /api/auth/social/:provider
POST /api/auth/social/:provider/callback
POST /api/auth/logout
GET /api/auth/me
POST /api/auth/refresh-token
POST /api/auth/forgot-password
POST /api/auth/reset-password
```
### Users and Profiles
```
GET /api/users/profile
PUT /api/users/profile
PUT /api/users/settings
GET /api/users/search?query=:query
```
### Contacts
```
GET /api/contacts
POST /api/contacts
GET /api/contacts/:id
PUT /api/contacts/:id
DELETE /api/contacts/:id
```
### Currencies and Exchange Rates
```
GET /api/currencies
GET /api/currencies/:code
GET /api/exchange-rates?from=:code&to=:code&date=:date
```
### Expenses
```
GET /api/expenses?page=:page&limit=:limit
POST /api/expenses
GET /api/expenses/:id
PUT /api/expenses/:id
DELETE /api/expenses/:id
POST /api/expenses/:id/receipt
```
### Expense Participants
```
GET /api/expenses/:expenseId/participants
POST /api/expenses/:expenseId/participants
PUT /api/expenses/:expenseId/participants/:id
DELETE /api/expenses/:expenseId/participants/:id
```
### Balances
```
GET /api/balances?currency=:code
GET /api/balances/:userId?currency=:code
GET /api/balances/summary?currency=:code
```
### Settlements
```
POST /api/settlements
GET /api/settlements?page=:page&limit=:limit
GET /api/settlements/:id
```
### Transactions (mostly internal)
```
GET /api/transactions?page=:page&limit=:limit
GET /api/transactions/:id
```
### Notifications
```
GET /api/notifications
PUT /api/notifications/:id/read
PUT /api/notifications/read-all
```
## API Request/Response Examples
### Creating an Expense
**Request:**
```json
POST /api/expenses
{
"title": "Dinner at Restaurant",
"description": "Birthday celebration",
"amount": 150.00,
"currency_code": "USD",
"date": "2023-05-15T19:30:00Z",
"payer_id": "550e8400-e29b-41d4-a716-446655440000",
"participants": [
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"share_amount": 50.00
},
{
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"share_amount": 50.00
},
{
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
"share_amount": 50.00
}
]
}
```
**Response:**
```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"title": "Dinner at Restaurant",
"description": "Birthday celebration",
"amount": 150.0,
"currency_code": "USD",
"date": "2023-05-15T19:30:00Z",
"payer_id": "550e8400-e29b-41d4-a716-446655440000",
"participants": [
{
"id": "d9d9e7b6-a5c5-4988-810f-f81b137e31b3",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"share_amount": 50.0,
"share_percentage": 33.33
},
{
"id": "8a8a8a8a-7425-40de-944b-e07fc1f90ae7",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"share_amount": 50.0,
"share_percentage": 33.33
},
{
"id": "9b9b9b9b-c54c-4b01-90e6-d701748f0851",
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
"share_amount": 50.0,
"share_percentage": 33.33
}
],
"created_at": "2023-05-15T19:35:00Z",
"updated_at": "2023-05-15T19:35:00Z"
}
```
### Viewing Balances
**Request:**
```
GET /api/balances?currency=USD
```
**Response:**
```json
{
"balances": [
{
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"display_name": "John Doe",
"profile_photo_url": "https://example.com/photos/john.jpg",
"currency_code": "USD",
"amount": -50.0,
"last_activity": "2023-05-15T19:35:00Z"
},
{
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
"display_name": "Jane Smith",
"profile_photo_url": "https://example.com/photos/jane.jpg",
"currency_code": "USD",
"amount": -50.0,
"last_activity": "2023-05-15T19:35:00Z"
}
],
"total_owed_to_you": 100.0,
"total_you_owe": 0.0,
"net_balance": 100.0
}
```
### Creating a Settlement
**Request:**
```json
POST /api/settlements
{
"payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"receiver_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 50.00,
"currency_code": "USD",
"date": "2023-05-16T14:00:00Z",
"memo": "Paying back for dinner"
}
```
**Response:**
```json
{
"id": "3a3a3a3a-58cc-4372-a567-0e02b2c3d479",
"payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payer_name": "John Doe",
"receiver_id": "550e8400-e29b-41d4-a716-446655440000",
"receiver_name": "Alice Johnson",
"amount": 50.0,
"currency_code": "USD",
"date": "2023-05-16T14:00:00Z",
"memo": "Paying back for dinner",
"created_at": "2023-05-16T14:02:00Z",
"updated_at": "2023-05-16T14:02:00Z"
}
```
## Implementation Considerations
### Double-Entry Bookkeeping Logic
1. **Transaction Creation**:
- Every expense and settlement must create balanced transactions
- System must ensure debits equal credits for all transactions
2. **Balance Calculation**:
- Balances are derived from the transaction ledger
- Each user's balance with another user is calculated by summing all transactions between them
- Negative balance means the user owes money; positive means they are owed money
### Multi-Currency Implementation
1. **Currency Conversion**:
- Use exchange rates from reliable sources (or integrate with exchange rate APIs)
- Store historical exchange rates for accurate reporting
- Implement conversion functions at the service layer
2. **User Preferences**:
- Allow users to set default currency
- Provide options to view balances in different currencies
3. **Settlement Handling**:
- Support settlements in different currencies
- Record the exchange rate used at settlement time
- Handle currency conversion fees if applicable
### Security and Data Integrity
1. **Transaction Atomicity**:
- Use database transactions to ensure all related changes succeed or fail together
- Implement validation to ensure expense shares sum to the total amount
2. **Audit Trail**:
- Log all financial operations
- Prevent modification of completed transactions
- Allow corrections through compensating transactions
3. **Data Validation**:
- Validate all currency amounts and exchange rates
- Ensure proper decimal precision handling
- Implement comprehensive input validation
## Conclusion
This architecture leverages double-entry bookkeeping principles to provide a robust foundation for tracking expenses and debts between users. The multi-currency support ensures the application can be used globally, with accurate conversion and balance tracking across different currencies.
The system design prioritizes data integrity, ensuring that all financial records are accurate, balanced, and traceable. By implementing this architecture, the Splitwise application will provide users with a reliable platform for managing shared expenses across multiple currencies.

302
docs/code-plan.md Normal file
View File

@ -0,0 +1,302 @@
# Splitwise Application - Code Plan
This document outlines the technical architecture, stack, and implementation plan for our Splitwise-style expense-splitting application.
## Tech Stack
### Frontend
- **Runtime**: Bun
- **Framework**: React 18+ with TypeScript
- **Build Tool**: Vite
- **Styling**: TailwindCSS with Shadcn UI component library
- **State Management**: Zustand
- **Data Fetching**: TanStack Query (React Query)
- **Form Handling**: React Hook Form with Zod validation
- **Routing**: React Router v6
- **Testing**: Vitest, React Testing Library
- **Components**: Shadcn UI (customizable Radix UI-based components)
### Backend
- **Runtime**: Bun
- **Language**: TypeScript
- **Framework**: Hono (lightweight, fast, TypeScript-first)
- **Database**: PostgreSQL
- **ORM**: Drizzle (TypeScript-first ORM)
- **Authentication**: better-auth (framework-agnostic authentication with email/password and social sign-on)
- **Validation**: Zod
- **File Storage**: Local storage with Sharp for image processing (can be migrated to S3 later)
- **Testing**: Bun test
## Architecture Overview
We'll implement a clean architecture with separation of concerns:
### Frontend Architecture
- **Application Layer**: React components, hooks, and contexts
- **Domain Layer**: Zustand stores and business logic
- **Infrastructure Layer**: API clients and external service integrations
#### Key Frontend Modules:
1. **Auth Module**: Sign-up, sign-in, profile management
2. **Expenses Module**: Create, edit, delete expenses
3. **Settlements Module**: Balances, settle up functionality
4. **Activity Module**: Feed of expense and settlement activities
5. **Notifications Module**: In-app notifications
6. **Settings Module**: User preferences and application settings
### Backend Architecture
- **API Layer**: Hono routes and controllers
- **Service Layer**: Business logic and core application services
- **Data Access Layer**: Drizzle ORM models and repositories
- **Domain Layer**: Entity definitions and domain logic
#### Key Backend Modules:
1. **Auth Module**: User authentication and authorization
2. **Users Module**: User management and profiles
3. **Contacts Module**: Contact relationships and invitations
4. **Expenses Module**: Expense tracking and splits
5. **Settlements Module**: Balance calculations and settlements
6. **Notifications Module**: Notification generation and delivery
7. **Files Module**: Receipt image upload and processing
## Database Schema (Initial)
### Users
- id: uuid (primary key)
- email: string (unique)
- display_name: string
- profile_photo_url: string (nullable)
- default_currency: string (default: 'USD')
- locale: string (default: 'en-US')
- timezone: string (default: 'UTC')
- created_at: timestamp
- updated_at: timestamp
### Auth (managed by better-auth)
- id: uuid (primary key)
- user_id: uuid (foreign key -> Users.id)
- provider: string (e.g., 'email', 'google', 'github')
- provider_user_id: string (nullable, for social providers)
- email: string
- password_hash: string (nullable, for email provider)
- refresh_token: string (nullable)
- last_login: timestamp
- created_at: timestamp
- updated_at: timestamp
### Contacts
- id: uuid (primary key)
- user_id: uuid (foreign key -> Users.id)
- contact_id: uuid (foreign key -> Users.id)
- status: enum ('pending', 'accepted', 'declined')
- created_at: timestamp
- updated_at: timestamp
### Expenses
- id: uuid (primary key)
- title: string
- amount: decimal
- date: timestamp
- payer_id: uuid (foreign key -> Users.id)
- memo: text (nullable)
- receipt_image_url: string (nullable)
- created_at: timestamp
- updated_at: timestamp
### ExpenseParticipants
- id: uuid (primary key)
- expense_id: uuid (foreign key -> Expenses.id)
- user_id: uuid (foreign key -> Users.id)
- share_amount: decimal
- created_at: timestamp
- updated_at: timestamp
### Settlements
- id: uuid (primary key)
- payer_id: uuid (foreign key -> Users.id)
- receiver_id: uuid (foreign key -> Users.id)
- amount: decimal
- date: timestamp
- memo: text (nullable)
- created_at: timestamp
- updated_at: timestamp
### Notifications
- id: uuid (primary key)
- user_id: uuid (foreign key -> Users.id)
- type: enum ('expense_added', 'expense_edited', 'settlement_received')
- content: jsonb
- is_read: boolean (default: false)
- created_at: timestamp
## API Endpoints
### Authentication (managed by better-auth)
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/social/:provider
- POST /api/auth/social/:provider/callback
- POST /api/auth/logout
- GET /api/auth/me
- POST /api/auth/refresh-token
- POST /api/auth/forgot-password
- POST /api/auth/reset-password
### Users
- GET /api/users/profile
- PUT /api/users/profile
- PUT /api/users/settings
### Contacts
- GET /api/contacts
- POST /api/contacts
- PUT /api/contacts/:id
- DELETE /api/contacts/:id
### Expenses
- GET /api/expenses
- POST /api/expenses
- GET /api/expenses/:id
- PUT /api/expenses/:id
- DELETE /api/expenses/:id
- POST /api/expenses/:id/receipt
### Balances
- GET /api/balances
- GET /api/balances/:contactId
### Settlements
- POST /api/settlements
- GET /api/settlements
- GET /api/settlements/:id
### Notifications
- GET /api/notifications
- PUT /api/notifications/:id/read
- PUT /api/notifications/read-all
## Implementation Plan
### Phase 1: Project Setup
1. Initialize frontend and backend projects with Bun
2. Set up TypeScript configuration
3. Configure Vite for the frontend
4. Install and configure Shadcn UI with TailwindCSS
5. Set up Hono for the backend
6. Configure Drizzle ORM with PostgreSQL
7. Set up development environment with Docker for database
### Phase 2: Core Authentication
1. Install and configure better-auth library
2. Set up authentication adapters for Hono and PostgreSQL
3. Configure email/password and social sign-on providers
4. Create authentication forms on frontend
5. Implement profile management
### Phase 3: Expense Tracking
1. Implement expense CRUD operations on backend
2. Create expense form components on frontend
3. Implement expense splitting logic
4. Add receipt image upload functionality
### Phase 4: Balances and Settlements
1. Implement balance calculation logic
2. Create settlement endpoints
3. Build balance visualization components
4. Implement settlement UI
### Phase 5: Activity Feed and Notifications
1. Create activity tracking for expenses and settlements
2. Implement notification generation
3. Build activity feed UI
4. Add in-app notification components
### Phase 6: Settings and Refinement
1. Implement user settings
2. Add locale and currency formatting
3. Refine UI/UX
4. Performance optimizations
## Folder Structure
### Frontend
```
frontend/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── auth/
│ │ ├── expenses/
│ │ ├── settlements/
│ │ ├── activity/
│ │ ├── notifications/
│ │ ├── settings/
│ │ └── ui/
│ ├── hooks/
│ │ └── useBetterAuth.ts
│ ├── pages/
│ ├── services/
│ │ ├── api.ts
│ │ └── auth.ts
│ ├── stores/
│ ├── types/
│ ├── utils/
│ ├── lib/
│ │ └── shadcn-ui/
│ ├── styles/
│ │ └── globals.css
│ ├── App.tsx
│ └── main.tsx
├── components.json
├── tailwind.config.js
├── .env
├── package.json
└── tsconfig.json
```
### Backend
```
backend/
├── src/
│ ├── controllers/
│ ├── db/
│ │ ├── schema/
│ │ └── migrations/
│ ├── middleware/
│ ├── services/
│ ├── auth/
│ │ ├── config.ts
│ │ ├── providers/
│ │ └── adapters/
│ ├── types/
│ ├── utils/
│ └── index.ts
├── .env
├── package.json
└── tsconfig.json
```
## Getting Started
### Prerequisites
- Bun installed
- PostgreSQL database
- Node.js 18+ (for additional tools)
### Setup Instructions
1. Clone the repository
2. Install dependencies for both frontend and backend with `bun install`
3. Set up environment variables (.env files)
4. Run database migrations with Drizzle
5. Start the development servers:
- Frontend: `bun run dev`
- Backend: `bun run dev`
## Development Practices
- TypeScript for type safety
- Feature branch workflow
- Write tests for critical functionality
- Use Zod schemas for validation and type inference
- Follow REST principles for API design
- Document API endpoints with JSDoc or Swagger

70
docs/features.md Normal file
View File

@ -0,0 +1,70 @@
# Features
This document outlines the core features for the MVP of the Splitwisestyle application, focusing on essential expensesplitting functionality without groups or advanced integrations.
## 1. User & Account
* **Sign Up / Sign In**
* Email/password authentication (OAuth support in later releases)
* **Profile**
* Display name, email, profile photo
* Default currency, locale, time zone
* **Contacts**
* Add/remove contacts by email or invite link
* Accept or decline contact requests
## 2. Expense Tracking
* **Add Expense**
* Title, total amount, date/time
* Payer (single user)
* Participants & split rules (equal or custom share)
* Optional memo/notes
* Upload receipt image (optional)
* **Edit & Delete**
* Modify any field or remove an expense
## 3. Balances & Settlements
* **Realtime Balances**
* “You owe / you are owed” percontact and overall summaries
* **Settle Up**
* Record a payment against one or more open balances
* Mark balances as paid to zero them out
## 4. Activity Feed / Change Log
* Chronological feed of who added, edited, or settled each expense
* Timestamps and links back to the full expense details
## 5. Notifications & Reminders
* **Inapp / Push Alerts**
* New expense involving you
* Someone settles up with you
* **Email Summaries** (optional for v1)
* Daily or weekly digest of outstanding balances
## 6. Settings & Preferences
* **Currency & Formatting**
* Set default currency (singlecurrency MVP)
* Date and number formats (e.g. DD/MM/YYYY)
* **Notification Controls**
* Toggle which alerts you receive
---
*End of MVP Feature List for initial release.*