This app is using React Router framework mode. It is hosted on Vercel, but in development it is run on a custom Express server.
For identity/auth, key management, and encryption, the app uses the Open Secret platform. The rest of the data is stored in a Postgres database hosted on Supabase. Any sensitive data is encrypted using keys from the Open Secret platform.
For more details about the architecture, see here.
We use Nix and devenv to set up the development environment. To start:
- Install
Nix(on macOS runcurl -L https://github.com/NixOS/experimental-nix-installer/releases/download/0.27.0/nix-installer.sh | sh -s -- install) - Install
devenv(on macOS runnix-env -iA devenv -f https://github.com/NixOS/nixpkgs/tarball/nixpkgs-unstable) - Install
direnv(for automatic shell activation):- on macOS run
brew install direnv - add the direnv hook to your shell as described here
- on macOS run
- Install the packages with
bun i
- Create
.envfile:
cp .env.example .envIf needed, update the .env file with alternative values. This file is ignored by git and used only for local development.
- Start Supabase local stack:
bun run supabase start- Run the dev server:
bun run devNote that when running locally the app is still using hosted Open Secret environment which is dedicated for the local development, while our hosted envs have their own dedicated Open Secret environments. This means that even while working on your machine you still need internet connection for identity/auth, key management, etc. Configurations for the local Open Secret environment can be seen in Agicash local project.
When testing the app on an actual mobile device, you need to connect to the same Wi-Fi as the machine hosting the app and access it via local IP or hostname. Unlike localhost or 127.0.0.1, those are not considered a safe context by the browser, so the browser APIs that require a safe context won't work. To solve this issue, you need to run the app on HTTPS instead. To run the dev server on HTTPS, execute:
bun run dev --httpsA self-signed certificate is used for HTTPS. The certificate is managed by devenv automatically. If you need to
regenerate the certificate (for example, if your local IP has changed), reload devenv by executing direnv reload
or run the certificate script directly by executing generate-ssl-cert.
Installing the root certificate on iOS: Mobile browsers require the root CA to be trusted. Find your mkcert root CA
by running mkcert -CAROOT (typically ~/Library/Application Support/mkcert/rootCA.pem), then AirDrop or email it to
your device. Install via Settings → General → VPN & Device Management, then enable trust in Settings → General →
About → Certificate Trust Settings.
master is the main branch. When working on a feature, branch off master and, when ready, make a PR back to master.
Try to make feature branches short-lived and concise (avoid implementing multiple features in one PR).
To update devenv CLI on your machine, for macOS run nix-env --upgrade --attr devenv -f https://github.com/NixOS/nixpkgs/tarball/nixpkgs-unstable.
To update devenv packages, run devenv update. When updating bun, make sure to update the engines version and version of the @types/bun
package in package.json and version specified in .github/actions/setup-environment/action.yml. When updating node, update the .nvmrc
file and engines version in package.json. Note that Vercel does not allow pinning to an exact node version, so in the package.json file,
we specify the max patch version, while in the .nvmrc, we specify the max version possible for that range because that is what Vercel will be
using when building, too.
First, build your app for production:
bun run buildThen run the app in production mode:
bun startThis starts an Express server that runs the React Router framework app. However, this is not what we are currently using in our hosted environments. We are using Vercel instead, but this gives us the option to deploy the app to a "full server" if we ever need to.
Currently, we have next and alpha environments and temporary preview deployments for individual features. The app is deployed to
Vercel. Every push to GitHub triggers a new Vercel deployment. Pushes to master branch are deploying a new next version. Pushes to
alpha branch are deploying a new alpha version.
The database and realtime service are hosted on the Supabase platform. Supabase branching
is used to manage different environments. We have next and alpha as Supabase persisted branches/environments. Database migrations
are applied automatically by Supabase whenever a new migration is merged to the corresponding Git branch. Preview deployments (feature
branches) are pointing to the next Supabase environment by default. However, if the preview deployment has some database migrations,
Supabase automatically creates an on-demand branch/environment and uses Supabase-Vercel integration to set the corresponding env variables
in Vercel to override the default setting that points the preview deployment to the next Supabase environment. The Supabase preview
branch/environment is automatically deleted once the feature branch is merged.
To release a new alpha version, make a pull request from master to the alpha branch.
alpha environment has dedicated Agicash Alpha
Open Secret environment.
next environment has dedicated Agicash Next
Open Secret environment.
All preview deployments of the Agicash app are also using Agicash Next Open Secret environment. However, since Supabase envs for preview deployments are created on demand and currently there is no way to configure static JWT secret for them, if you want the preview deployment to work with Open Secret you need to copy the shared secret value used by Agicash Next Open Secret env and create the a new JWT signing key in the corresponding Supabase env. To do that:
- Find the current value of the secret used by Agicash Next Open Secret env (ask other devs to share it)
- Go to Supabase dashboard and in the JWT Keys section of the corresponding Supabase project settings create a new Standby Key. Pick
HS256 (Shared Secret)signing algorithm, select to import existing secret and paste the secret there. Make sure not to selectSecret is already Base64 encoded. Once the key is created, then rotate the keys to make the new one used by Supabase.
If there is a new Agicash app environment that needs to have a dedicated Open Secret environment, create a new project in Open Secret cloud dashboard. Things that need to be configured are:
- Google OAuth settings - Open Secret needs Google Auth client id and secret. You can get those in the Google Cloud Console from the existing client or create a new one.
- Resend email settings - Open Secret uses Resend platform to send emails and needs the API key. You can create the API key here. Additionally you need to configure the email to send from (e.g. noreply@agi.cash) and URL for email verification link (when user receives an email for email address verification, the email will contain this link).
- Third-Party JWT Secret - Open Secret creates JWTs that are then used by Supabase to authorize if the user should have access to the requested data. For this to work, Open Secret and Supabase need to share the secret that signed the JWT. To create a secret, go to Supabase dashboard and in the JWT Keys section of the corresponding Supabase project settings create a new Standby Key (pick
HS256 (Shared Secret)signing algorithm) and then rotate the keys to make newly created one used by Supabase. Once that is done, paste the same secret into Open Secret dashboard.
Lastly we need to point the Agicash app to the new Open Secret environment. To do that set VITE_OPEN_SECRET_CLIENT_ID env variable to the client ID of the created Open Secret project.
A dependency should be added only if the benefits are clear. Avoid adding it for trivial stuff. Any dependency added
should be inspected and pinned to an exact version (bun add <package_name>@<version> --exact). For any dependency added
to the client side, be mindful of the bundle size. Bundlephobia can be used to check the
total size of the dependency (the actual impact on the app bundle size could be smaller if we are using only some
elements and the lib supports tree shaking).
Auth data (user ID, email, etc.) is stored on the Open Secret platform. The rest of the data is stored in a Postgres database
hosted on Supabase. Sensitive data is encrypted with the key from the Open Secret platform, which is accessible only to the
logged-in user. While developing locally, the local Supabase stack is used. To start it, run bun run supabase start command.
To stop it, run bun run supabase stop command. The start command will start the database and realtime service, plus other
services useful for development, like Supabase Studio. You can use Supabase Studio to inspect the database and run queries.
Supabase is configured in the supabase/config.toml file.
Schema changes to the Postgres database should be done using migrations. Migrations are stored in the supabase/migrations
folder. Always try to make the schema changes in a backward compatible way.
Database migrations can be done in two ways:
- Using Supabase Studio. With this approach you can make db changes directly to your local database in the Studio UI
and then run
bun supabase db diff --file <MIGRATION_NAME>to create a migration file for the changes. - Using
bun supabase migration new <MIGRATION_NAME>. This command will create a new empty migration file in thesupabase/migrationsfolder where you can then write the SQL commands to make the changes. To apply the migration to the local database runbun supabase db push.
To keep the db TypeScript types in sync with the database schema run bun run db:generate-types command. If you forget
to run this command after making changes to the database, the types will be updated by the pre-commit hook. To skip the
pre-commit hook, use --no-verify param with git commit command. This can be useful when committing temporary code, but
the CI will check if the types are up to date and, if not, will not allow merging to master.
To reset the local database, run bun supabase db reset. Note that this will delete any existing local data and run all
migrations on a clean database.
Migrations are applied to hosted envs automatically by the Supabase platform. You can track the migrations applied in the Supabase dashboard by going to the branches page and checking the logs for the respective branch. If the migration fails for the feature branch you can reapply it by just resetting the branch. If it fails for the persisted or production you will need to resolve the issue and push migrations from your machine by doing:
- Switch to the persisted/production branch by running
git checkout <branch_name>(e.g.git checkout masterfor next env) and make sure you have the latest version by runninggit pull origin <branch_name>. - Run
bun supabase loginto log in to the Supabase dashboard so the CLI can access it. - Run
bun supabase linkto link to the remote project. For this, you will need the database password. Ask other team members for the current db password (the password can also be reset from the dashboard if needed). - Run
bun supabase db pushto apply migrations to the remote database.
Steps 2 and 3 can be skipped if you have already logged in and linked the project before.
Type checking is separated from build and is performed using TypeScript compiler. To run type check manually run
bun run typecheck command.
Biome is used for code linting and formatting. Supported commands:
bun run lint- runs linter and performs safe fixesbun run lint:check- runs lint check onlybun run format- runs formatter and performs fixesbun run format:check- runs format check onlybun run fix:all- runs type checking, linter and formatter and performs safe fixesbun run fix:staged- runs type checking, linter and formatter on git staged files only and performs safe fixesbun run check:all- runs type, lint and format checks only
Types, formatting and code styles are enforced using pre-commit hook. For a nicer development experience, it is recommended
to enable auto linting and formatting on file save in IDE. Instructions for that can be found here.
Pre-commit hook is configured using devenv (see the devenv.nix file), and it runs bun run fix:staged command. To skip the
pre-commit hook use --no-verify param with git commit command. This can be useful when committing temporary code but
the CI will run the checks again and won't allow any non-conforming code to be committed to master.
Lint & formatting configs are defined in the biome.jsonc file.
The idea is to cover key lower-level reusable pieces with unit tests and main app flows with e2e Playwright tests. We are not aiming for any specific coverage. Use your best judgment.
Bun is used to run unit tests. To run them use bun test or bun run test. Colocate the test file next to the piece it
tests and name the file <name_of_the_unit_tested>.test.ts(x).
E2e tests are written in Playwright. In these tests we are mocking the Open Secret API so tests
can be run offline, and so we can simulate any desired Open Secret behavior. For some examples on how to use the mocking
see the existing tests. E2e tests can be found in the top-level e2e folder. To run them use bun run test:e2e (add --ui
param to run them in Playwright UI). The tests will also start a local Supabase stack, if it is not already running.
New e2e test suites should be added to the e2e folder and named <name_of_the_suite>.spec.ts.
Every pull request created will trigger the GitHub Actions CI pipeline. The pipeline runs three jobs in parallel. One
checks code format, lint, and types. Another runs the unit tests, and a third one runs e2e tests. If any of the jobs fail,
merging to master will not be allowed.
Gift card images should use the WebP format for better compression and faster loading.
Use the provided conversion script:
# Convert a single file
./tools/convert-to-webp.sh app/assets/gift-cards/mycard.png
# Convert all PNGs in the gift-cards directory
./tools/convert-to-webp.sh --dir app/assets/gift-cards
# With custom quality (default is 80)
./tools/convert-to-webp.sh -q 85 --dir app/assets/gift-cardsRequires cwebp - install with brew install webp or run via nix:
nix shell nixpkgs#libwebp --command ./tools/convert-to-webp.sh --dir app/assets/gift-cards