.
├── .api-client <- autogenerated api client
├── config <- configurations for migrations and environment variables
├── database/
│ └── migrations <- database migrations goes here
└── src/
├── core <- logging, validation, error handling modules
├── lib <- code that may be fully separated from the app
├── modules/
│ └── example/
│ ├── application/
│ │ ├── dto <- data transfer objects
│ │ ├── exceptions <- application exceptions
│ │ ├── services <- interfaces of services
│ │ ├── use-cases <- abstractions and implementation of use-cases
│ │ └── handlers <- event handlers
│ ├── domain/
│ │ ├── entities <- something with id
│ │ ├── value-objects <- object that represent simple structure without id or logic
│ │ ├── enums <- enumerations
│ │ ├── repositories <- repository interfaces
│ │ └── events <- domain events classes
│ └── infrastructure/
│ ├── controllers <- rest controllers
│ ├── handlers <- infrastructure event handlers
│ └── persistence <- database related logic/
│ └── drizzle/
│ ├── mappers
│ └── repositories
└── shared <- contains all base implementation and database schema
- Install dependencies
pnpm install- Generate and run migrations
To do so, you will need to make DB_URL variable available in your environment
Windows:
$env:DB_URL="db url here"Unix:
export DB_URL="db url here"pnpm run db:generate
pnpm run db:push- Run the app
pnpm run start:devConfiguration is managed through environment-specific shell scripts stored in config/ directory. The system uses SOPS (Secrets OPerationS) with PGP encryption to securely manage secrets in version control.
config/
├── .env # Local overrides (gitignored)
├── config.staging.sh # Plaintext staging config (gitignored)
├── config.staging.enc.sh # Encrypted staging config (committed to git)
└── drizzle.config.ts # Database configuration
For local development, you have two options:
Option 1: Using .env file
Option 2: Using environment variables
# Export variables directly in your shell
export SUPABASE_URL=your-local-supabase-url
export SUPABASE_SECRET_KEY=your-key
# ... other variablesYou can introspect available configuration in AppConfigModel here.
It defines validation using class-validator. Implicit type coercion is enabled, so strings may be converted to number, boolean or other types.
Prerequisites:
Install SOPS and GPG:
macOS:
brew install sops gnupgLinux (Ubuntu/Debian):
# Install GPG (usually pre-installed)
sudo apt-get update && sudo apt-get install -y gnupg
# Install SOPS
SOPS_VERSION="3.11.0"
wget "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64"
sudo mv sops-${SOPS_VERSION}.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sopsWindows:
# Install using Chocolatey
choco install sops gnupg
# Or using Scoop
scoop install sops gpgGet PGP private key from 1Password project vault
Import PGP key (one-time setup):
# Copy the key from 1Password, then run:
gpg --import
# Paste the key (including BEGIN/END lines) and press Ctrl+D (Unix/macOS) or Ctrl+Z (Windows)View encrypted config:
sops config/config.staging.enc.shEdit encrypted config:
# Opens in your default editor, automatically encrypts on save
sops config/config.staging.enc.shDecrypt to plaintext (for local testing):
sops decrypt config/config.staging.enc.sh > config/config.staging.shEncrypt changes:
# If you edited the plaintext file
sops encrypt config/config.staging.sh > config/config.staging.enc.shNote: SOPS automatically finds
.sops.yamlby walking up the directory tree, so you can run commands fromapi/directory or project root.
When forking this template for a new project:
- Generate a new PGP key pair:
gpg --batch --gen-key <<EOF
Key-Type: RSA
Key-Length: 4096
Name-Real: Your Project Name Config
Name-Email: [email protected]
Expire-Date: 0
%no-protection
%commit
EOF- Get the key fingerprint:
gpg --list-keys --keyid-format LONG "Your Project Name Config"
# Copy the fingerprint (40-character hex string)- Update
.sops.yamlin project root:
creation_rules:
- path_regex: '.*api/config/config.staging.enc.sh$'
pgp: 'YOUR_NEW_FINGERPRINT_HERE'- Export and store keys in 1Password:
# Export private key to terminal, then copy and save in 1Password
gpg --armor --export-secret-keys YOUR_FINGERPRINT
# Export public key to terminal (for team members to import)
gpg --armor --export YOUR_FINGERPRINT-
Add PGP private key to GitHub Secrets:
- Go to repository Settings → Secrets and variables → Actions
- Create new secret named
PGP_PRIVATE_KEY - Paste the entire private key (including BEGIN/END lines)
-
Create and encrypt your config:
# Create plaintext config with your values
cp config/config.staging.sh config/config.staging.sh.backup
# Edit config/config.staging.sh with your values
sops encrypt config/config.staging.sh > config/config.staging.enc.sh- Commit encrypted config:
# Note: Only commit .sops.yaml if you changed the PGP fingerprint
git add .sops.yaml config/config.staging.enc.sh
git commit -m "feat: configure project-specific secrets"Note: Once .sops.yaml is configured with your project's PGP fingerprint, you don't need to update it again. Just edit config.staging.enc.sh directly with sops when you need to change secrets.
For new team members who need to work with encrypted configs:
- Get the PGP private key from 1Password project vault
- Import it (see "Import PGP key" section above)
- Now you can use
sopscommands to view/edit encrypted configs
Managing Multiple Projects:
If you work on multiple projects with different PGP keys, GPG handles this automatically:
- Import all project keys once:
gpg --import(paste each key) - SOPS automatically selects the correct key based on
.sops.yaml - List all imported keys:
gpg --list-keys
To identify which key belongs to which project, use descriptive names when generating keys (e.g., "Project X Config" instead of generic names).
If you need to remove a key:
# List keys with their IDs
gpg --list-keys
# Delete a key
gpg --delete-secret-keys FINGERPRINT
gpg --delete-keys FINGERPRINT- Never commit plaintext
.shfiles - they are gitignored - Store PGP keys in 1Password - project vault, not personal
- Rotate keys if compromised - generate new PGP pair and re-encrypt all configs
To configure supabase auth, you need to provide following variables:
SUPABASE_URL=???
SUPABASE_SECRET_KEY=???
JWT_SECRET=??? # used for signing jwt tokens by supabaseAlso, we support google oauth2, which requires additional configuration:
Go to Authentication > Providers > Google
Enable Google auth and provide clientId and clientSecret
Additionally, for GitHub OAuth2 configuration:
- Go to Authentication > Providers > GitHub
- Enable GitHub auth and provide
clientIdandclientSecret - Important: Enable the "Skip nonce checks" option
⚠️ Warning: Enabling "Skip nonce checks" is necessary for Google OAuth to work properly with Supabase.
Also, due to new supabase policies, only users in your organization will receive emails from supabase SMTP. For broader testing and production, don't forget to set up your own SMTP server.
To setup stripe billing, you need to provide following variables:
STRIPE_API_KEY=???
STRIPE_WEBHOOK_SIGNING_SECRET=???To add products you will be required to do it through stripe UI.
To make a product available in the list retrieved from the API, you need to set a specific metadata key-value pair in the Stripe UI:
⚠️ Warning: Only products with the metadata key "type" set to "plan" will be returned by the API. This is a current limitation and may change in future updates. We are considering implementing a script to seed products automatically, which would streamline this process. This behavior is implemented inStripeSubscriptionPlanService:listPlansandgetPlanByIdmethods.
How we store billing data: We don't store products on our side, to avoid synchronization issues and errors. Payment customer data and subscription details are stored in our database.
Currently we are using drizzle ORM with postgres. We define database schema in src/shared/infrastructure/database/schema/public-database-schema.ts file.
To keep our system stable we utilize database transactions. To do so, we use unit of work pattern. This pattern is implemented in DrizzleDbContext class, that creates and provides repositories to our use-cases.
See the example of repositories creation here
private initRepositories() {
this._userRepository = new DrizzleUserRepository(this._db);
this._paymentCustomerRepository = new DrizzlePaymentCustomerRepository(this._db);
}
⚠️ Important Note: Transactions are not implemented yet in the current version of DrizzleDbContext.
The current implementation of DrizzleDbContext does not provide transaction functionality. Methods like
startTransaction(),commitTransaction(), androllbackTransaction()are placeholders and do not perform actual database transactions. This limitation should be considered when working with the database, especially for operations that require atomic transactions. Future updates will aim to implement proper transaction support to ensure data integrity and consistency across multiple database operations.
To deploy our app and api client you will need to do the next:
To deploy api client, create .npmrc file in the api/.api-client folder with the next content:
@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=GITHUB_TOKEN_WITH_ACCESS_TO_OUR_NPM_REGISTRYTo get github token with access to our npm registry, you need to go to github.com/settings/tokens and create new personal access token with repo and read:packages scopes.
Don't forget to change package name to your project-specific name in package.json file.
After that you will be able publish api client:
cd api/.api-client
pnpm publishAll deployment IaC terraform and github workflows are pre-defined in the repository. You will need to just change app name in terraform code and workflow files.
API and Web are deploying as single app: api is available at /api path and web at / path.

