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:
parent
de91da311f
commit
74b7ddf3d6
34
backend/.gitignore
vendored
Normal file
34
backend/.gitignore
vendored
Normal 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
15
backend/README.md
Normal 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
341
backend/bun.lock
Normal 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
12
backend/drizzle.config.ts
Normal 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
1
backend/index.ts
Normal file
@ -0,0 +1 @@
|
||||
console.log("Hello via Bun!");
|
27
backend/package.json
Normal file
27
backend/package.json
Normal 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
24
backend/src/db/index.ts
Normal 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
14
backend/src/db/migrate.ts
Normal 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();
|
39
backend/src/db/schema/currencies.ts
Normal file
39
backend/src/db/schema/currencies.ts
Normal 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] }),
|
||||
};
|
||||
});
|
33
backend/src/db/schema/expenses.ts
Normal file
33
backend/src/db/schema/expenses.ts
Normal 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] }),
|
||||
};
|
||||
});
|
7
backend/src/db/schema/index.ts
Normal file
7
backend/src/db/schema/index.ts
Normal 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';
|
12
backend/src/db/schema/notifications.ts
Normal file
12
backend/src/db/schema/notifications.ts
Normal 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(),
|
||||
});
|
16
backend/src/db/schema/settlements.ts
Normal file
16
backend/src/db/schema/settlements.ts
Normal 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(),
|
||||
});
|
22
backend/src/db/schema/transactions.ts
Normal file
22
backend/src/db/schema/transactions.ts
Normal 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(),
|
||||
});
|
46
backend/src/db/schema/users.ts
Normal file
46
backend/src/db/schema/users.ts
Normal 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
80
backend/src/index.ts
Normal 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
38
backend/tsconfig.json
Normal 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
585
docs/architecture.md
Normal 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
302
docs/code-plan.md
Normal 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
70
docs/features.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Features
|
||||
|
||||
This document outlines the core features for the MVP of the Splitwise‑style application, focusing on essential expense‑splitting 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
|
||||
|
||||
* **Real‑time Balances**
|
||||
|
||||
* “You owe / you are owed” per‑contact 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
|
||||
|
||||
* **In‑app / 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 (single‑currency 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.*
|
||||
|
Loading…
Reference in New Issue
Block a user