BillingApp is a multi-tenant internal operations and client-portal application built on top of OAuth/local authentication and tenant-aware RBAC.
It combines:
- user authentication and invitations
- organization and project management
- role-based access control
- client onboarding
- timesheet capture and approvals
- billing rates
- invoice generation
- a restricted client invoice portal
This README is the operator guide for the current application behavior.
The app is designed for organizations that need to manage delivery work and turn approved effort into client invoices while keeping access tightly controlled.
In practical terms, the app allows you to:
- create organizations and projects
- invite internal users and client users
- assign users to projects
- capture billable time
- approve or reject submitted time
- define billing rates
- generate invoices from approved billable time
- let clients log in and view only their own invoice information
- Authentication
- local email/password
- OAuth/OIDC providers
- magic-link invite acceptance
- RBAC and tenancy
- organizations are the tenant boundary
- project membership is separate from organization membership
- Client management
- client records belong to an organization
- client users are linked to a specific client record
- Timesheets
- internal users log time against projects
- managers/admins review and approve
- Billing
- finance/admin roles define rates and create invoices
- Client portal
- client users get a limited read-only invoice experience
The app currently uses two role families:
These are the original admin and member roles used across the RBAC system:
Super Admin- full platform access
- can manage all organizations, projects, users, audit, billing, and assignments
Agency Admin- high-level admin within agency-style usage
- can create/manage organizations and projects in scope
- can invite users in allowed scope
Agency Company Admin- scoped admin role below Agency Admin
- can manage organizations/projects in allowed scope
- can invite allowed subordinate users
Agency User- legacy participant role
- not sufficient by itself for the newer timesheet workflow
Company Admin- company-scoped admin role
- can manage organizations/projects in allowed scope
- can invite allowed users
Company User- legacy participant role
Individual User- limited creator role for simpler scenarios
These are the roles used by the newer timesheet and billing modules:
employee- internal contributor
- can log and submit their own timesheets
project_manager- can review submitted timesheets for projects they manage
- can approve/reject time
finance- can manage billing rates
- can generate invoices
- can update invoice lifecycle state
org_admin- tenant-scoped admin role
- can manage operational setup inside the org
client- external/client portal role
- can only view client-safe invoice information for their linked client
Timesheets and billing are built around the modern operational roles.
That means:
Agency Useralone is not the same thing asemployeeCompany Useralone is not the same thing asemployee
If someone must enter timesheets, they should have a role such as:
employee- or
project_manager
and they should be associated with the relevant project.
Typical workflow:
- sign in
- create or manage organizations
- create or manage projects
- invite admins, internal users, and clients
- review audit
- oversee billing setup and invoice flow
Typical workflow:
- sign in
- select the active organization
- create or edit projects
- create clients
- invite internal users and client users
- assign internal users to projects
- monitor timesheets and approvals depending on role
- manage billing/invoices if role allows it
Typical workflow:
- sign in
- go to
Timesheets - choose project/task/date/hours/notes
- save draft
- submit entry
- wait for approval or rejection
Typical workflow:
- sign in
- go to
Timesheets - open the approval queue
- review submitted entries in managed project scope
- approve or reject entries
Typical workflow:
- sign in
- go to
Billing - create billing rates
- review unbilled approved hours
- generate draft invoice
- review invoice
- mark invoice sent
- later mark invoice paid
Typical workflow:
- receive magic-link invite
- accept invite and log in
- see client portal navigation only
- view invoice list
- open invoice detail
- open printable invoice view if needed
Client users should not see or use:
- timesheets
- projects
- clients admin page
- organizations
- users
- assignments
- audit
- internal billing controls
- Sign in with an admin-capable role.
- Go to
Users. - Select the target organization in the app header.
- In the
Invite Usercard:- enter email
- choose the correct role
- choose the organization
- Click
Invite. - The app creates a magic link.
- The latest invite link is shown in the UI and copied to the clipboard.
- The invited user opens the link, sets a password, and joins.
A client user cannot be invited as a free-floating user. They must be linked to a real client record.
Required flow:
- Go to
Clients. - Create the client record first.
- Go to
Users. - Choose role
client. - Choose the organization.
- Choose the
Client accountfrom the dropdown. - Click
Invite.
What happens:
- the invite is created
- the user is linked to the selected organization
- the membership is linked to the selected
client_id - the user receives client-portal access, not internal workspace access
If the organization already has a client record, for example northstar, and you want to add another contact such as Jamie:
- Do not create another client record if Jamie belongs to the same client account.
- Go to
Users. - Choose role
client. - Choose the same organization.
- Choose the same existing client account, for example
northstar. - Click
Invite.
That creates another client user tied to the same client record.
You can have multiple client records in the same organization.
The only rule is:
- client names must be unique within the same organization
Examples:
- allowed in same org:
NorthstarAcmeContoso
- not allowed in same org:
Northstar- another
Northstar
If you truly need two separate client records for one company, use distinct names such as:
Northstar - USNorthstar - AdvisoryNorthstar - Division B
Internal users only.
In practice:
employeecan log and submit timesheetsproject_managercan review and approve- admin roles can see more depending on scope
clientcannot use timesheets
Legacy roles like Agency User or Company User are not enough on their own for the new timesheet workflow unless they are also given a modern operational role such as employee.
- The user must belong to the organization.
- The user must have a timesheet-capable role such as
employee. - The user should be associated with the project through
Assignments. - The project should be active.
- Sign in as an internal timesheet-capable user.
- Go to
Timesheets. - In
Log new entry:- select project
- select date
- enter hours
- optionally select task
- optionally use the project default client
- enter description
- Click
Save draft. - The entry appears in the current period list.
- In
Current period entries, find the draft entry. - Click
Submit.
Primary approvers:
project_manager
Also allowed depending on scope:
org_admin- higher admin roles
- Project manager/admin opens
Timesheets. - Reviews the
Approval queue. - Approves or rejects entries.
- Approved billable entries become eligible for invoice generation.
Roles that typically do this:
financeorg_admin- admin roles with billing access
Steps:
- Go to
Billing. - In
Billing rate:- optionally select client
- optionally select project
- optionally set a role
- set hourly rate
- set currency
- set effective date
- Click
Save rate.
Interpretation:
- if project is set, the rate is project-scoped
- if only client is set, it is client-scoped
- if neither is set, it behaves as a broader fallback
Roles that typically do this:
financeorg_admin- admin roles with billing access
Steps:
- Ensure there are approved, billable timesheet entries.
- Go to
Billing. - In
Generate invoice:- choose client
- choose period start
- choose period end
- choose issue date
- choose grouping mode
- optionally add internal notes
- Click
Generate draft invoice. - The draft invoice appears in the invoice list.
The client portal is intended to be read-only.
Flow:
- Internal finance/admin user generates draft invoice.
- Internal finance/admin user opens the invoice.
- Internal finance/admin user clicks
Mark sent. - Client user logs in.
- Client sees only non-draft client-visible invoices in the portal.
Important:
- client users cannot mark invoices sent
- client users cannot change invoice status
- client users cannot see internal billing controls
Internal users see the full app shell depending on role:
- Dashboard
- Clients
- Timesheets
- Billing
- Organizations
- Projects
- Users
- Assignments
- Audit/access tools depending on privilege
Client users see a restricted portal:
- Dashboard
- Invoices
They are redirected away from internal-only routes back to the invoice portal.
Current behavior:
- clicking an invoice opens an in-place modal preview
- from the modal, users can open a printable full document route
- internal users get operational actions in the modal
- client users get read-only invoice preview in the modal
- Python 3.11+
- Node.js and npm
- PostgreSQL or Neon Postgres
- optional OAuth app credentials if you want third-party login providers
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
uvicorn app.main:app --host 127.0.0.1 --port 8000 --reloadBackend API docs:
http://127.0.0.1:8000/docs
cd frontend
npm install
npm run devFrontend app:
http://localhost:3000
You can start both backend and frontend with:
.\run.batThe current run.bat:
- starts backend on
127.0.0.1:8000 - starts frontend on
localhost:3000 - uses
.venv\Scripts\python.exeif available - uses the direct Node/npm path on Windows to avoid broken npm shim issues
The repository now includes backend container assets:
- Dockerfile
- docker-compose.backend.yml
- .dockerignore
- alembic.ini
- alembic
- blaxel.toml
- blaxel.yaml
- entrypoint.sh
Build the backend image:
docker build -t billing-backend:prod .Run the backend container with the production env file:
docker compose -f docker-compose.backend.yml up --buildThe compose startup runs Alembic before the app server:
alembic upgrade headThe container exposes:
- backend on
http://localhost:8000 - health check on
http://localhost:8000/healthz
For Blaxel sandbox deployment, the container now:
- copies
sandbox-apifromghcr.io/blaxel-ai/sandbox:latest - starts
sandbox-apion the reserved internal port - runs
alembic upgrade head - launches the FastAPI backend through the sandbox process API
This repository now includes two Blaxel-specific production files:
- blaxel.toml
- used by
bl deploy - configured for a Blaxel
sandbox, not an agent
- used by
- blaxel.yaml
- used by
bl apply -f blaxel.yaml - declarative manifest for a Blaxel
Sandbox
- used by
Typical usage:
bl deployAdvanced manifest usage:
bl apply -f blaxel.yamlThese files are meant for mirroring the backend as a sandbox image on Blaxel.
Before using blaxel.yaml, replace the placeholder image:
your-registry.example.com/your-team/billing-backend-sandbox:latestwith the real image that Blaxel can pull.
Create a root .env file. You can use .env_example.txt as a starting point.
For production, use .env.prod as the template and set the same values in your hosting platforms.
DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require
SECRET_KEY=replace-with-a-real-secret
FRONTEND_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000The frontend reads:
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_COOKIE_MODE=falseNotes:
NEXT_PUBLIC_API_URLshould point to the backend base URLNEXT_PUBLIC_COOKIE_MODEdefaults tofalse
Add only the providers you actually want to enable. The app supports multiple providers including:
- Apple
- Microsoft
- Twitter/X
- GitHub
- GitLab
- Discord
- Amazon
- Dropbox
Examples:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
MICROSOFT_CLIENT_ID=your_microsoft_client_id
MICROSOFT_CLIENT_SECRET=your_microsoft_client_secretOAuth callback pattern:
http://localhost:8000/auth/{provider}/callback
Example:
http://localhost:8000/auth/github/callback
- backend on Blaxel.ai
- frontend on Vercel
- database on Neon Postgres or any hosted Postgres with SSL enabled
Root production template:
APP_ENV=production
ENV_FILE=.env.prod
DATABASE_URL=postgresql://billing_user:StrongPassword123@ep-cool-db-123456.ap-southeast-1.aws.neon.tech/billingdb?sslmode=require
SECRET_KEY=replace-with-a-long-random-secret
FRONTEND_URL=https://billing-app.vercel.app
BACKEND_PUBLIC_URL=https://billing-api.blaxel.ai
CORS_ORIGINS=https://billing-app.vercel.app,https://billing-app-git-main-vinodvv2023.vercel.app
TRUSTED_HOSTS=billing-app.vercel.app,billing-api.blaxel.ai
SESSION_COOKIE_SECURE=true
SESSION_SAME_SITE=lax
SESSION_COOKIE_DOMAIN=
FORWARDED_ALLOW_IPS=*
NEXT_PUBLIC_API_URL=https://billing-api.blaxel.ai
NEXT_PUBLIC_COOKIE_MODE=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=Set these in your Blaxel backend service:
APP_ENV=production
ENV_FILE=.env.prod
DATABASE_URL=postgresql://billing_user:StrongPassword123@ep-cool-db-123456.ap-southeast-1.aws.neon.tech/billingdb?sslmode=require
SECRET_KEY=replace-with-a-long-random-secret
FRONTEND_URL=https://billing-app.vercel.app
CORS_ORIGINS=https://billing-app.vercel.app,https://billing-app-git-main-vinodvv2023.vercel.app
TRUSTED_HOSTS=billing-app.vercel.app,billing-api.blaxel.ai
SESSION_COOKIE_SECURE=true
SESSION_SAME_SITE=lax
SESSION_COOKIE_DOMAIN=
FORWARDED_ALLOW_IPS=*
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=Notes:
FRONTEND_URLis used for OAuth callback redirection back to the frontendCORS_ORIGINSmust include every frontend origin allowed to call the backendTRUSTED_HOSTSshould include the public frontend and backend hostnamesSESSION_COOKIE_SECURE=trueshould remain enabled in production- preview or branch Vercel domains should be added only if you want them to work against the production backend
Set these in the Vercel project:
NEXT_PUBLIC_API_URL=https://billing-api.blaxel.ai
NEXT_PUBLIC_COOKIE_MODE=falseNotes:
NEXT_PUBLIC_API_URLmust point to the public backend base URL- the frontend no longer uses
localhostfallback in production, so this value should always be set in Vercel
Backend provider callback URLs should point to the backend:
- Google:
https://billing-api.blaxel.ai/auth/google/callback
- GitHub:
https://billing-api.blaxel.ai/auth/github/callback
- Microsoft:
https://billing-api.blaxel.ai/auth/microsoft/callback
- Twitter/X:
https://billing-api.blaxel.ai/auth/twitter/callback
The frontend receives the final post-login redirect through:
https://billing-app.vercel.app/oauth/callback
For magic-link invite acceptance, the frontend route is:
https://billing-app.vercel.app/oauth/magic
- Set backend env vars in Blaxel
- Set frontend env vars in Vercel
- Confirm
DATABASE_URLincludes SSL requirements - Confirm
FRONTEND_URLmatches the real Vercel production domain - Confirm
CORS_ORIGINSincludes all allowed frontend domains - Confirm every OAuth provider uses the backend callback URL, not the frontend URL
- Deploy backend
- Deploy frontend
- Test local login
- Test OAuth login
- Test magic-link invite acceptance
- Test client portal invoice visibility
This repository now includes Alembic scaffolding for production-safe schema management.
Local migration commands:
alembic upgrade head
alembic downgrade -1Important:
- development still auto-creates tables if they do not exist
- production does not run
Base.metadata.create_all() - production should always use Alembic migrations
- Production is intended for PostgreSQL/Neon
- tests can run with SQLite
- the repository includes SQL migration files under
docs/migrations
- Create
.env - Install backend dependencies
- Install frontend dependencies
- Start backend
- Start frontend
- Run DB migration if needed
- Open the app
- Create organization
- Create project
- Create client
- Invite internal users
- Invite client users
If timesheet entry does not work:
- confirm the user is not a
client - confirm the user has a modern timesheet role such as
employee - confirm the user is assigned to the project
If client invite does not work:
- confirm a client record exists first
- confirm role is
client - confirm a client account is selected during invite
If client cannot see invoice:
- confirm invoice is not still
draft - confirm the client user is linked to the correct
client_id - confirm the project and invoice belong to that same client
- App:
http://localhost:3000 - Dashboard:
http://localhost:3000/dashboard - Clients:
http://localhost:3000/clients - Timesheets:
http://localhost:3000/timesheets - Billing:
http://localhost:3000/billing - Organizations:
http://localhost:3000/organizations - Projects:
http://localhost:3000/projects - Users:
http://localhost:3000/users - Assignments:
http://localhost:3000/assignments - Access Matrix:
http://localhost:3000/access-matrix - API docs:
http://127.0.0.1:8000/docs
- Client users belong to a client record, not directly to project membership.
- A project is associated to a client through
projects.client_id. - Client visibility is derived from matching
client_idwithin the selected organization. - Internal timesheet and billing flows are intentionally separated from the client portal.