Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ jobs:
# Apply only new migrations (does not reset existing data)
supabase db push --linked

- name: Sync Stripe prices to billing_plans
env:
STRIPE_SECRET_KEY: ${{ secrets.PRODUCTION_STRIPE_SECRET_KEY }}
SUPABASE_URL: ${{ secrets.PRODUCTION_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.PRODUCTION_SUPABASE_SERVICE_ROLE_KEY }}
run: |
set -euo pipefail
npm run billing:sync-stripe

- name: Ensure Stripe webhook endpoint (production)
id: stripe_webhook_production
env:
STRIPE_SECRET_KEY: ${{ secrets.PRODUCTION_STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_URL: ${{ secrets.PRODUCTION_SUPABASE_URL }}/functions/v1/stripe-webhook
STRIPE_WEBHOOK_SECRET: ${{ secrets.PRODUCTION_STRIPE_WEBHOOK_SECRET }}
WEBHOOK_DESCRIPTION: BeakerStack production (${{ secrets.PRODUCTION_SUPABASE_PROJECT_REF }})
run: node scripts/ensure-stripe-webhook-endpoint.mjs

- name: Deploy Supabase Edge Functions
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
PRODUCTION_SUPABASE_PROJECT_REF: ${{ secrets.PRODUCTION_SUPABASE_PROJECT_REF }}
PRODUCTION_SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_SUPABASE_DB_PASSWORD }}
PRODUCTION_SUPABASE_URL: ${{ secrets.PRODUCTION_SUPABASE_URL }}
PRODUCTION_SUPABASE_ANON_KEY: ${{ secrets.PRODUCTION_SUPABASE_ANON_KEY }}
PRODUCTION_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.PRODUCTION_SUPABASE_SERVICE_ROLE_KEY }}
PRODUCTION_STRIPE_SECRET_KEY: ${{ secrets.PRODUCTION_STRIPE_SECRET_KEY }}
PRODUCTION_STRIPE_WEBHOOK_SECRET: ${{ steps.stripe_webhook_production.outputs.resolved_webhook_secret }}
PRODUCTION_BILLING_ALLOWED_ORIGINS: ${{ secrets.PRODUCTION_BILLING_ALLOWED_ORIGINS }}
run: |
set -euo pipefail
cd supabase
supabase link \
--project-ref "${PRODUCTION_SUPABASE_PROJECT_REF}" \
--password "${PRODUCTION_SUPABASE_DB_PASSWORD}" \
--yes
supabase secrets set \
STRIPE_SECRET_KEY="${PRODUCTION_STRIPE_SECRET_KEY}" \
STRIPE_WEBHOOK_SECRET="${PRODUCTION_STRIPE_WEBHOOK_SECRET}" \
BILLING_SUPABASE_URL="${PRODUCTION_SUPABASE_URL}" \
BILLING_SUPABASE_ANON_KEY="${PRODUCTION_SUPABASE_ANON_KEY}" \
BILLING_SUPABASE_SERVICE_ROLE_KEY="${PRODUCTION_SUPABASE_SERVICE_ROLE_KEY}" \
BILLING_ALLOWED_ORIGINS="${PRODUCTION_BILLING_ALLOWED_ORIGINS}"
supabase functions deploy stripe-webhook billing-stripe \
--project-ref "${PRODUCTION_SUPABASE_PROJECT_REF}"

- name: Resolve infrastructure outputs
id: infra
shell: bash
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ jobs:
# Apply only new migrations (does not reset existing data)
supabase db push --linked

- name: Sync Stripe prices to billing_plans
env:
STRIPE_SECRET_KEY: ${{ secrets.STAGING_STRIPE_SECRET_KEY }}
SUPABASE_URL: ${{ secrets.STAGING_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.STAGING_SUPABASE_SERVICE_ROLE_KEY }}
run: |
set -euo pipefail
npm run billing:sync-stripe

- name: Ensure Stripe webhook endpoint (staging)
id: stripe_webhook_staging
env:
STRIPE_SECRET_KEY: ${{ secrets.STAGING_STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_URL: ${{ secrets.STAGING_SUPABASE_URL }}/functions/v1/stripe-webhook
STRIPE_WEBHOOK_SECRET: ${{ secrets.STAGING_STRIPE_WEBHOOK_SECRET }}
WEBHOOK_DESCRIPTION: BeakerStack staging (${{ secrets.STAGING_SUPABASE_PROJECT_REF }})
run: node scripts/ensure-stripe-webhook-endpoint.mjs

- name: Deploy Supabase Edge Functions
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
STAGING_SUPABASE_PROJECT_REF: ${{ secrets.STAGING_SUPABASE_PROJECT_REF }}
STAGING_SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_SUPABASE_DB_PASSWORD }}
STAGING_SUPABASE_URL: ${{ secrets.STAGING_SUPABASE_URL }}
STAGING_SUPABASE_ANON_KEY: ${{ secrets.STAGING_SUPABASE_ANON_KEY }}
STAGING_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.STAGING_SUPABASE_SERVICE_ROLE_KEY }}
STAGING_STRIPE_SECRET_KEY: ${{ secrets.STAGING_STRIPE_SECRET_KEY }}
STAGING_STRIPE_WEBHOOK_SECRET: ${{ steps.stripe_webhook_staging.outputs.resolved_webhook_secret }}
STAGING_BILLING_ALLOWED_ORIGINS: ${{ secrets.STAGING_BILLING_ALLOWED_ORIGINS }}
run: |
set -euo pipefail
cd supabase
supabase link \
--project-ref "${STAGING_SUPABASE_PROJECT_REF}" \
--password "${STAGING_SUPABASE_DB_PASSWORD}" \
--yes
supabase secrets set \
STRIPE_SECRET_KEY="${STAGING_STRIPE_SECRET_KEY}" \
STRIPE_WEBHOOK_SECRET="${STAGING_STRIPE_WEBHOOK_SECRET}" \
BILLING_SUPABASE_URL="${STAGING_SUPABASE_URL}" \
BILLING_SUPABASE_ANON_KEY="${STAGING_SUPABASE_ANON_KEY}" \
BILLING_SUPABASE_SERVICE_ROLE_KEY="${STAGING_SUPABASE_SERVICE_ROLE_KEY}" \
BILLING_ALLOWED_ORIGINS="${STAGING_BILLING_ALLOWED_ORIGINS}"
supabase functions deploy stripe-webhook billing-stripe \
--project-ref "${STAGING_SUPABASE_PROJECT_REF}"

- name: Resolve infrastructure outputs
id: infra
shell: bash
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/pr-preview-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,55 @@ jobs:
--baseline-ref "${{ github.base_ref }}" \
--skip-if-unchanged

- name: Sync Stripe prices to preview billing_plans
env:
STRIPE_SECRET_KEY: ${{ secrets.PREVIEW_STRIPE_SECRET_KEY }}
SUPABASE_URL: ${{ secrets.PREVIEW_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.PR_TESTING_SUPABASE_SERVICE_ROLE_KEY }}
run: |
set -euo pipefail
npm run billing:sync-stripe

- name: Ensure Stripe webhook endpoint (preview)
id: stripe_webhook_preview
env:
STRIPE_SECRET_KEY: ${{ secrets.PREVIEW_STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_URL: ${{ secrets.PREVIEW_SUPABASE_URL }}/functions/v1/stripe-webhook
STRIPE_WEBHOOK_SECRET: ${{ secrets.PREVIEW_STRIPE_WEBHOOK_SECRET }}
WEBHOOK_DESCRIPTION: BeakerStack preview (${{ secrets.SUPABASE_PREVIEW_PROJECT_REF }})
run: node scripts/ensure-stripe-webhook-endpoint.mjs

- name: Deploy preview billing edge functions
shell: bash
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PREVIEW_PROJECT_REF: ${{ secrets.SUPABASE_PREVIEW_PROJECT_REF }}
SUPABASE_PREVIEW_DB_PASSWORD: ${{ secrets.SUPABASE_PREVIEW_DB_PASSWORD }}
PREVIEW_SUPABASE_URL: ${{ secrets.PREVIEW_SUPABASE_URL }}
PREVIEW_SUPABASE_ANON_KEY: ${{ secrets.PREVIEW_SUPABASE_ANON_KEY }}
PR_TESTING_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.PR_TESTING_SUPABASE_SERVICE_ROLE_KEY }}
PREVIEW_STRIPE_SECRET_KEY: ${{ secrets.PREVIEW_STRIPE_SECRET_KEY }}
PREVIEW_STRIPE_WEBHOOK_SECRET: ${{ steps.stripe_webhook_preview.outputs.resolved_webhook_secret }}
PREVIEW_BILLING_ALLOWED_ORIGINS: ${{ secrets.PREVIEW_BILLING_ALLOWED_ORIGINS }}
run: |
set -euo pipefail
cd supabase
# Path-based previews use this host; empty allowlist breaks checkout redirect validation.
BILLING_ORIGINS="${PREVIEW_BILLING_ALLOWED_ORIGINS:-https://deploy.beakerstack.com}"
supabase link \
--project-ref "${SUPABASE_PREVIEW_PROJECT_REF}" \
--password "${SUPABASE_PREVIEW_DB_PASSWORD}" \
--yes
supabase secrets set \
STRIPE_SECRET_KEY="${PREVIEW_STRIPE_SECRET_KEY}" \
STRIPE_WEBHOOK_SECRET="${PREVIEW_STRIPE_WEBHOOK_SECRET}" \
BILLING_SUPABASE_URL="${PREVIEW_SUPABASE_URL}" \
BILLING_SUPABASE_ANON_KEY="${PREVIEW_SUPABASE_ANON_KEY}" \
BILLING_SUPABASE_SERVICE_ROLE_KEY="${PR_TESTING_SUPABASE_SERVICE_ROLE_KEY}" \
BILLING_ALLOWED_ORIGINS="${BILLING_ORIGINS}"
supabase functions deploy stripe-webhook billing-stripe \
--project-ref "${SUPABASE_PREVIEW_PROJECT_REF}"

- name: Build and deploy web preview
id: web
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ jobs:
apps/web/coverage
apps/mobile/coverage
packages/shared-tests/coverage
packages/billing/coverage
if-no-files-found: warn

- name: Upload test results
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,9 @@ apps/mobile/google-services.json

# AWS build artifacts
.aws-build/

# Agent / IDE skill trees (Stripe docs mirror + symlinks); install locally, do not commit
.agents/
.augment/skills/
.claude/skills/
skills-lock.json
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,10 +550,12 @@ You can:

### Migration Development Workflow

Schema migrations live **only** under **`supabase/migrations/` at the repository root**. Author migrations (`supabase migration new …`), run **`supabase start`** and **`supabase db reset`** from the **repo root**, even when you are only working on the mobile app. The `apps/mobile/supabase/` tree can keep a mobile-specific `config.toml` for local auth callbacks; it does not hold a second copy of SQL migrations (see `apps/mobile/supabase/migrations/README.md`).

#### For Simple Migrations (New Columns, Indexes)

```bash
# 1. Develop locally
# 1. Develop locally (from repository root)
supabase start
supabase migration new add_user_bio

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ The specific machinery (tiered Supabase, AWS static hosting with PR paths, EAS c

**[Get started → QUICKSTART.md](QUICKSTART.md)** — **Use this template**, local “hello world” in minutes, full-cloud checklist when you are ready.

| Need | Doc |
| --------------------- | ---------------------------------- |
| Full topic index | [docs/README.md](docs/README.md) |
| Environments & design | [ARCHITECTURE.md](ARCHITECTURE.md) |
| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
| Need | Doc |
| --------------------- | ------------------------------------------------------------ |
| Full topic index | [docs/README.md](docs/README.md) |
| Stripe billing setup | [docs/stripe-billing-setup.md](docs/stripe-billing-setup.md) |
| Environments & design | [ARCHITECTURE.md](ARCHITECTURE.md) |
| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |

## Features

Expand Down Expand Up @@ -100,6 +101,7 @@ BeakerStack/

### Database

- **`supabase/migrations/` is canonical at the repo root** — run `supabase migration new …`, `supabase start`, and `supabase db reset` from the repository root (mobile developers: see [`apps/mobile/supabase/migrations/README.md`](apps/mobile/supabase/migrations/README.md)).
- `supabase start` / `supabase stop` — Local Supabase
- `npm run gen:types` — TypeScript types from DB
- `supabase db reset` — Reset local DB
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Mobile app (Expo)

Most scripts run from the **repository root** (`npm run mobile`, etc.). See [`docs/guides/MOBILE.md`](../../docs/guides/MOBILE.md).

## Local Supabase schema

Database migrations live only under **`supabase/migrations/`** at the repo root. From the BeakerStack checkout root, run `supabase start`, `supabase migration new …`, and `supabase db reset` — do not expect duplicated `.sql` files under [`supabase/migrations/`](./supabase/migrations/) (see the policy note there).

Optional [`supabase/config.toml`](./supabase/config.toml) here keeps mobile-oriented auth redirect URLs separate from the root config; it is not the source of migration SQL.
25 changes: 25 additions & 0 deletions apps/mobile/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,31 @@ jest.mock('@beakerstack/shared/components/forms/FormError.native', () => ({
},
}));

// Avoid loading @beakerstack/billing in Jest (package uses TS paths Jest does not resolve like Metro)
jest.mock('../src/screens/BillingScreen', () => {
const { View, Text } = require('react-native');
return {
__esModule: true,
default: () => (
<View testID='billing-screen'>
<Text>Billing</Text>
</View>
),
};
});

jest.mock('../src/screens/DashboardScreen', () => {
const { View, Text } = require('react-native');
return {
__esModule: true,
default: () => (
<View testID='dashboard-screen'>
<Text>Dashboard</Text>
</View>
),
};
});

import { render } from '@testing-library/react-native';
import { describe, it, expect } from '@jest/globals';
import App from '../App';
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/__tests__/components/AvatarUpload.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ jest.mock('expo-image-picker', () => ({
MediaTypeOptions: {
Images: 'Images',
},
UIImagePickerPresentationStyle: {
FULL_SCREEN: 'fullScreen',
PAGE_SHEET: 'pageSheet',
FORM_SHEET: 'formSheet',
CURRENT_CONTEXT: 'currentContext',
OVER_FULL_SCREEN: 'overFullScreen',
OVER_CURRENT_CONTEXT: 'overCurrentContext',
POPOVER: 'popover',
AUTOMATIC: 'automatic',
},
}));

// Mock expo-file-system
Expand Down
44 changes: 31 additions & 13 deletions apps/mobile/__tests__/navigation/AppNavigator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ jest.mock('../../src/screens/ProfileScreen', () => {
);
});

jest.mock('../../src/screens/BillingScreen', () => {
const { View, Text } = require('react-native');
return () => (
<View testID='billing-screen'>
<Text>Billing Screen</Text>
</View>
);
});

describe('AppNavigator', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -89,18 +98,27 @@ describe('AppNavigator', () => {
global.__DEV__ = originalDev;
});

it.skip('does not expose navigation ref in production', () => {
// Skip - __DEV__ is read-only in Jest environment
const originalDev = __DEV__;
// @ts-expect-error test-only assignment to global __DEV__
global.__DEV__ = false;

render(<AppNavigator />);

// Navigation ref should not be exposed in production
expect((global as any).navigationRef).toBeUndefined();

// @ts-expect-error test-only assignment to global __DEV__
global.__DEV__ = originalDev;
it('does not expose navigation ref when __DEV__ is false', () => {
const g = globalThis as { __DEV__?: boolean; navigationRef?: unknown };
const originalDev = g.__DEV__;
try {
delete (g as { navigationRef?: unknown }).navigationRef;

Object.defineProperty(g, '__DEV__', {
value: false,
configurable: true,
writable: true,
});

render(<AppNavigator />);

expect(g.navigationRef).toBeUndefined();
} finally {
Object.defineProperty(g, '__DEV__', {
value: originalDev,
configurable: true,
writable: true,
});
}
});
});
Loading
Loading