A self-hosted money management app to help grownups manage "bank accounts"* for the kids in their life.
The app shows the current balance for each kid added to the app. Transactions (called adjustments here) can add or subtract money to a kid’s balance.
Just like a real bank account, interest can add—or subtract—money from their balance over time. To help kids think more about saving money, the app shows how much interest they will earn over the next 7, 30, and 365 days.
Also, kids can set a savings goal and see their progress and an estimate of how long it will take to reach it.
* "Bank account" as in not a real bank account. This doesn’t integrate with any sort of external system, and how the money is owned and paid to the kid is up to you.
This project splits up the front-end and back-end server into two separate directories.
The front-end lives in the ./front-end directory and has its own package.json file. The front-end is compiled using Vite, and the production code is served from the ./front-end/dist directory.
The back-end lives in the ./server directory, and it has its own set of Node dependencies. The server is not compiled, and it is meant to be run directly from the server directory.
- Copy
./front-end/.env.exampleto./front-end/.env. - If you prefer, you can change the URL that the server runs on by changing the
VITE_PUBLIC_API_URLvalue. - Run
npm cito install dependencies. - Run
npm run buildto build out the front-end. - Point your web directory to the
./front-end/distdirectory.
- Copy
./server/.env.exampleto./server/.env. - You must set the
DATABASE_URLvalue to point to a SQLite database. The other values are optional. - Run
npm cito install dependencies. - Run database migration to set up the SQLite database. This will create the SQLite database file if one isn’t already created:
npm run prisma:migrate:up
- Run
npm run startto start the server.
Right now the only way to create a user is via a node script run on the command line:
node ./server/scripts/create-user.js --u=my-username --p=my-password --adminIn this script, the --admin argument is optional, but it’s needed to eneble the Settings page for the user to manage kids in the app, as well as to add or remove money from kids’ balances. You can set --admin=false to remove admin status from a user.
To update a user’s password or admin status, you can run the update-user script:
node ./server/scripts/update-user.js --u=my-username --p=my-new-password --admin=falseWhen updating a user, the username is a required field and the username cannot be changed. To change a username, you can create a new user with the new username and delete the existing user with the delete-user script:
node ./server/scripts/delete-user.js --u=my-usernameNote
All users have access to all kids and adjustments. Deleting a user will not delete any data, other than the user login credentials and admin status.
If you want to give access to kids, or to create a non-admin user, you can use the create-user script and omit the --admin argument:
node ./server/scripts/create-user.js --u=my-username --p=my-passwordThis user will be able to view all kids and adjustments, but they will not be able to visit the Settings page, or add new adjustments.
The Settings page will show a list of all the kids in the app with a list of settings associated with each kid. To change a setting, click on the current value, and it will pop up an input field. Make your change to the field and click elsewhere to make the popover go away. Once the setting has been saved, the kid’s data will show the new value in the list.
To add a new kid, use the + Add Kid button at the top of the page. To remove a kid, take note of the ID for the kid, then click Remove Kid. This will ask you to enter in the ID for that kid to verify that you want to remove them from the app.
Note
This ID is also the ID to use in CLI commands. The ID is automatically generated for the kid and will not change.
If you have created a non-admin user for a kid, you can link the user with a kid on the Settings page. To do this, find the user under the Users section, click on the Non-admin button, and select the kid from the dropdown field.
The next time the kid logs into the app, they will be able to set their own savings goal on their own page.
If you want to set up a cron job, or if you prefer the command line for things, you can use the add-adjustments script to add (or remove) money for a specific kid.
In general, this starts with calling the command and passing in the ID for the kid:
node ./server/scripts/add-adjustment.js --kid=1From there you will need to pass in arguments based on the type of adjustment you are trying to make. Here are some examples:
node ./server/scripts/add-adjustment.js --kid=1 --dollar=5This adds $5 to the balance for the kid with the ID, 1.
node ./server/scripts/add-adjustment.js --kid=1 --interestThis will increase the balance for the kid with the ID, 1, by using the settings for the kid, interest and interestThresholds, to calculate the amount of interest to add to the balance.
Note
You can only add either a dollar or interest adjustment per time the command is run. If you add arguments for both, you’ll get the dollar adjustment.
Hosting requires two different setups:
- A web directory that points to the
./front-end/distdirectory - A Node.js server that can run the app
These can be hosted on one server that can do both of these things, or on separate servers.
What makes this app tricky to host is that the code in the server directory needs to be hosted on a server where the server will continue to listen for API calls and where database files are allowed (which makes this harder to host on Netlify and Vercel). One option you can try out is to change the DATABASE_URL environment value to use a database that you have hosted elsewhere.
The connection to the database is done through Prisma, so you can check out these options for setting up an external database connection.
While this isn’t fully tested, the app should run just fine—although maybe a little slower—and no other changes are needed, outside what is required to host a static site and Node.js server.
You can host this project on a VPS (or similar hosting environment) as long as the following requirements can be met:
- The VPS’ NGINX or Apache server lets you proxy the port your app is running on (such as
:3000) - Node 24+ is installed, and you can SSH into the server to use it (BONUS: if you can use it in a deploy script, that’s even better)
As an example, here’s how you could host this project on a Laravel Forge-provisioned VPS:
- Create a new site and set the web root to:
/front-end/dist - Set your DNS to point to the server.
- In NGINX settings, replace the
location /block with the following (you can choose a different port to run the app if you’d like). Change theFRONT_END_URLto the URL you’ll visit in the browser:location /api/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_hide_header 'Access-Control-Allow-Origin'; add_header 'Access-Control-Allow-Origin' 'FRONT_END_URL'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' 'FRONT_END_URL'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; add_header 'Access-Control-Max-Age' 1728000; # Cache preflight for 20 days add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } } - Pull down this repo and move all the files to the site’s directory root. If you put it anywhere other than in the site root, that’s okay, but you’ll need to update the web root to point to the
/front-end/distfolder. - Install NVM (if you want to use it to manage node), otherwise make sure Node 24+ is installed and running as the server’s default Node version.
- Set up a new Background Process, in Processes > Background Processes.
- Change the directory to an absolute path to the
serverdirectory. - Set the command to:
npm run start - After the process is set up, copy the ID for the process to use it in the next step.
- Change the directory to an absolute path to the
- Add these scripts to your deployment settings to build and update everything, replacing
SITE_NAMEwith the name of your site as it appears in Forge. Also replaceBACKGROUND_PROCESS_IDwith the ID of the Background Process you created in the previous step:cd /home/forge/SITE_NAME git pull origin $FORGE_SITE_BRANCH . ~/.nvm/nvm.sh # Install and build the front-end cd /home/forge/SITE_NAME/front-end nvm use npm install npm run build # Install and start the server cd /home/forge/SITE_NAME/server nvm use npm install npm run prisma:generate npm run prisma:migrate:up # Restart the background process sudo -S supervisorctl restart daemon-BACKGROUND_PROCESS_ID:*
You can create a nightly backup of the database by trigger the backup-db Node script in a cron job. Again, using Forge as an example, you can create a new job in Processes > Scheduler > Scheduled jobs.
Again, the SITE_NAME can be replaced with the name of your site as it appears in Forge.
/home/forge/.nvm/versions/node/v24.15.0/bin/node /home/forge/SITE_NAME/server/scripts/backup-db.jsThis will create a copy of the SQLite database file in the /home/forge/SITE_NAME/server/backups directory. For redundancy, it would be a good idea to set up another job to move that file to another location that is off of your hosting server.
Another scheduled job can be set up to automatically add interest every night:
/home/forge/.nvm/versions/node/v24.15.0/bin/node /home/forge/SITE_NAME/server/scripts/add-adjustment.js --kid=1 --interestUpon successful runs, you’ll see a confirmation in the log, noting which kid got updated and by how much.