Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

PointUp

Track airline miles, hotel points, credit card rewards, and every other loyalty currency of value in one place. Built as a modern, fully-typed TypeScript monorepo with a framework-agnostic core (DDD / hexagonal architecture), a Next.js web surface, Drizzle ORM on PostgreSQL, and AWS infrastructure as code.

This is the point_bot repository. PointUp is the modernized successor to the original Python/Selenium PointBot (now under legacy/python-selenium/). See docs/migration-from-pointup.md for the port, the decisions behind it, and the AWS Bedrock (Claude Sonnet) assistant wiring.

LayerTechnology
CoreNative TypeScript domain/application/infrastructure layers (@pointup/core)
Web surfaceNext.js 16 App Router + React 19 + Tailwind CSS 4
DataPostgreSQL via Drizzle ORM + drizzle-kit migrations
UsersClerk user management (email, social logins, MFA, user profiles)
API client@pointup/api-client — typed, fetch-only, runs on web, mobile, and browser extensions
TestingVitest unit tests over ports-and-adapters fakes
InfrastructureAWS CDK — ECS Fargate, ALB, RDS PostgreSQL, Secrets Manager
CIGitHub Actions (lint, typecheck, test, build, CDK synth)

Documentation

  • docs/roadmap.md — feature roadmap: what's shipped, what's next, and how it's sequenced
  • docs/api.md — full API v1 reference with request/response examples
  • docs/architecture.md — DDD layering, SOLID mapping, workspace layout
  • docs/multi-surface.md — adding mobile apps and browser extensions
  • docs/integrations.md — loyalty providers (airlines, hotels, credit cards, rail, shopping) and credential vaults (1Password, Apple Keychain, Chrome)
  • docs/brand.md — brand kit: logo assets, color tokens, typography, voice
  • docs/migration-from-pointup.md — how the modernization was ported into point_bot, feature-parity checklist, and the Bedrock assistant
  • docs/bot.md — the PointBot chat surface: Slack/Discord commands, the Notifier port, digests, and deployment
  • docs/extension.md — the Chrome extension: capture balances from provider pages via @pointup/api-client

Repository layout

├── apps/
│ ├── web/ # Next.js app: pages, components, API routes, composition root
│ │ ├── src/components/ # branded UI components (logo, cards, forms)
│ │ └── public/brand/ # brand kit assets (SVG logomarks, lockup)
│ ├── worker/ # Background jobs: scheduled syncs + email + chat digests
│ ├── bot/ # PointBot chat surface: Slack/Discord commands over the core
│ └── extension/ # Chrome (MV3) extension: capture balances from provider pages
├── packages/
│ ├── core/ # Domain + application + infrastructure (framework-free)
│ │ ├── src/domain/ # entities, provider catalog, repository ports, errors
│ │ ├── src/application/ # use cases + outbound ports (gateway, vault, clock)
│ │ ├── src/contracts/ # zod wire schemas shared by all surfaces
│ │ ├── src/infrastructure/ # Drizzle repos, provider gateways, vault adapters
│ │ └── drizzle/ # generated SQL migrations
│ └── api-client/ # Typed HTTP client for mobile / extension surfaces
├── infra/ # AWS CDK app (standalone package)
└── docs/ # Architecture and integration guides

Local development

Requirements: Node.js ≥ 20 and Docker (for the local database).

# 1. Install all workspaces
npm install
# 2. Configure environment
cp .env-example .env
# Fill in your Clerk keys from https://dashboard.clerk.com (API keys)# 3. Start PostgreSQL
docker compose up -d db
# 4. Apply database migrations
npm run db:migrate
# 5. Run the dev server
npm run dev

Root scripts

CommandDescription
npm run devStart the Next.js dev server
npm run buildProduction build of the web app
npm run lintESLint across the whole monorepo
npm run typecheckTypeScript checking in every workspace
npm run testVitest unit tests (core use cases, no DB needed)
npm run db:generateGenerate a new SQL migration from schema changes
npm run db:migrateApply pending migrations to DATABASE_URL
npm run db:studioOpen Drizzle Studio to browse the database

Database workflow

The schema lives in packages/core/src/infrastructure/db/schema.ts. After editing it:

npm run db:generate # writes SQL to packages/core/drizzle/
npm run db:migrate # applies it to DATABASE_URL

Commit the generated migration files — they are the source of truth for production.

To browse the database with an open-source admin UI (Adminer):

docker compose --profile tools up -d # http://localhost:8081

Drizzle Studio (npm run db:studio) is also available for a schema-aware view.

Background jobs

The worker (apps/worker) runs the same core use cases outside the request path:

# Refresh every user's balances
docker compose run --rm worker sync
# Send portfolio digest emails (delivered to Mailpit locally)
docker compose --profile tools up -d # Mailpit UI: http://localhost:8025
docker compose run --rm worker digest
# Apply pending database migrations (same job CI runs on deploy)
docker compose run --rm worker migrate

Without Docker: DATABASE_URL=... npm run dev --workspace @pointup/worker -- sync. Emails route through the Mailer port — Mailpit (OSS) over SMTP locally, AWS SES in production, or plain console logging when nothing is configured.

User management

Users, sessions, sign-in flows, MFA, and profiles are handled by Clerk. Create an application in the Clerk dashboard and copy the publishable + secret keys into .env. The application database stores only Clerk user ids next to domain data — there are no local user/password tables to operate.

API (v1)

All surfaces speak the same versioned API; shapes are defined in @pointup/core/contracts. See docs/api.md for the full reference with request/response examples.

Method & pathAuthDescription
GET /api/healthALB health check
GET /api/v1/providersSupported programs (airlines, hotels, credit cards, rail, shopping)
GET /api/v1/summarysessionPortfolio totals, per-kind breakdown, last sync
GET /api/v1/exportsessionDownload accounts + history (?format=json|csv)
POST /api/v1/importsessionRehydrate accounts + balances from a CSV export
GET /api/v1/calendar.icssessioniCal feed of account expiration dates
GET /api/v1/goalssessionTrip goals with progress against balances
POST /api/v1/goalssessionCreate a trip goal
PATCH /api/v1/goals/{id}sessionUpdate a trip goal
DELETE /api/v1/goals/{id}sessionDelete a trip goal
POST /api/v1/demosessionSeed sample portfolio (empty accounts only)
GET /api/v1/sharessessionList privacy-preserving share links
POST /api/v1/sharessessionCreate a share link
DELETE /api/v1/shares/{id}sessionRevoke a share link
GET /api/v1/public/share/{token}Public portfolio snapshot (no membership numbers)
GET /api/v1/loyalty-accounts/deletedsessionSoft-deleted accounts in the restore window
POST /api/v1/loyalty-accounts/{id}/restoresessionUndo an unlink
POST /api/v1/assistant/chatsessionGrounded AI portfolio assistant
GET /api/v1/value-advicesessionTransfer rankings + bang-for-buck deals
POST /api/v1/deals/scrapesessionScrape a deal URL and re-rank advice
GET /api/v1/activitysessionChronological activity feed
GET /api/v1/expiringsessionAccounts expiring within N days (default 90)
GET /api/v1/loyalty-accountssessionLinked accounts with latest balances and trends
POST /api/v1/loyalty-accountssessionLink a program membership
GET /api/v1/loyalty-accounts/{id}sessionOne account with its latest balance
PATCH /api/v1/loyalty-accounts/{id}sessionUpdate membership number / credential ref
DELETE /api/v1/loyalty-accounts/{id}sessionUnlink the account (history cascades)
GET /api/v1/loyalty-accounts/{id}/balancessessionBalance history, newest first (?limit=1..365)
POST /api/v1/loyalty-accounts/{id}/balancessessionRecord a manually observed balance
POST /api/v1/loyalty-accounts/{id}/syncsessionFetch and record the current balance (accepts an optional one-time transientCredential)
POST /api/v1/syncsessionSync every linked account; per-account outcomes

Errors are uniform: { "error": { "code": "DUPLICATE_LOYALTY_ACCOUNT", "message": "..." } } with stable codes from the domain layer.

Deploying to AWS

All infrastructure is defined with the AWS CDK in infra/:

  • VPC with public, private (egress) and isolated subnets across two AZs
  • RDS PostgreSQL 17 in isolated subnets, credentials auto-generated in Secrets Manager, storage encryption, 7-day backups, deletion protection
  • ECS Fargate service (2+ tasks, CPU-based autoscaling to 6) behind a public Application Load Balancer with /api/health health checks and deployment circuit breaker
  • Docker image built from the repository Dockerfile (monorepo-aware, standalone Next.js output) at deploy time and pushed to a CDK-managed ECR repository
  • Secrets Manager secret for the Clerk secret key (placeholder — set the real value after the first deploy); the Clerk publishable key is passed as a Docker build arg since it is inlined into the client bundle
  • Scheduled worker tasks (EventBridge → Fargate, from Dockerfile.worker): balance syncs every 6 hours and a weekly digest email job on Mondays
  • SES for digest delivery — verify a sender identity, then deploy with -c digestFromEmail=digest@yourdomain.com (without it the digest job logs instead of sending)
  • CloudWatch alarms on ALB 5xx responses and sustained service CPU
cd infra
npm install
# One-time per account/region
npx cdk bootstrap
# Deploy (builds and pushes the Docker image, then updates the stack)
npx cdk deploy

After the first deploy:

  1. Set the real Clerk secret key in the secret printed as ClerkSecretArn:

    aws secretsmanager put-secret-value \
    --secret-id <ClerkSecretArn> --secret-string 'sk_live_...'
  2. Deploy with your real Clerk publishable key so it is baked into the client bundle:

    npx cdk deploy -c clerkPublishableKey=pk_live_...
  3. Run the database migrations against RDS (e.g. from a bastion host or an ECS one-off task):

    DATABASE_URL="postgresql://..." npm run db:migrate
  4. Add http://<LoadBalancerUrl> (or your domain) to the allowed origins in the Clerk dashboard.

  5. To enable digest emails, verify a sender identity in SES (and move out of the SES sandbox for real recipients), then redeploy with:

    npx cdk deploy -c clerkPublishableKey=pk_live_... -c digestFromEmail=digest@yourdomain.com

For production, add an ACM certificate and a Route 53 hosted zone to ApplicationLoadBalancedFargateService in infra/lib/app-stack.ts to enable HTTPS, and consider enabling multiAz on the database plus a second NAT gateway.

Continuous deployment

Every push to master deploys automatically via .github/workflows/deploy.yml: full verification (lint, typecheck, tests, builds) → cdk deploy (builds and pushes both Docker images, updates the stack) → database migrations as a one-off Fargate task (the worker image's migrate job, which applies pending drizzle migrations under an advisory lock so concurrent runs serialize).

Authentication uses GitHub OIDC federation — no long-lived AWS keys are stored in the repository. One-time setup:

# 1. Create the OIDC provider + deploy role (in infra/)
npx cdk deploy GithubOidc -c githubRepo=<owner>/<repo># 2. In GitHub repo settings, add:# Secret AWS_DEPLOY_ROLE_ARN = DeployRoleArn output from step 1# Secret CLERK_PUBLISHABLE_KEY = pk_live_... (inlined into the client bundle)# Variable AWS_REGION = deployment region (optional, default us-east-1)# Variable DIGEST_FROM_EMAIL = verified SES sender (optional)

The deploy role's permissions are minimal: it can only assume the CDK bootstrap roles and run the migration task. Until AWS_DEPLOY_ROLE_ARN is configured, the workflow verifies the build and skips deployment. Pull requests run the CI workflow (checks only, no AWS access).

Running the full stack in Docker locally

docker compose --profile app up --build

This starts PostgreSQL and the production image of the app on http://localhost:3000.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages