@launchframe/mcp 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/dist/content/auth/overview.md +3 -3
- package/dist/content/{content/database → database}/schema.md +0 -12
- package/dist/content/variants/overview.md +1 -1
- package/package.json +2 -2
- package/dist/content/content/auth/overview.md +0 -64
- package/dist/content/content/credits/deduction.md +0 -27
- package/dist/content/content/credits/strategies.md +0 -25
- package/dist/content/content/crons/pattern.md +0 -51
- package/dist/content/content/entities/conventions.md +0 -97
- package/dist/content/content/env/conventions.md +0 -123
- package/dist/content/content/feature-gates/overview.md +0 -74
- package/dist/content/content/modules/structure.md +0 -44
- package/dist/content/content/queues/names.md +0 -18
- package/dist/content/content/variants/overview.md +0 -67
- package/dist/content/content/webhooks/architecture.md +0 -53
|
@@ -15,14 +15,14 @@ All routes are protected by default via the global `BetterAuthGuard` (registered
|
|
|
15
15
|
|------|-------------|
|
|
16
16
|
| `business_user` | Default role for all registered users |
|
|
17
17
|
| `superadmin` | Granted via admin panel; full access |
|
|
18
|
-
| `
|
|
18
|
+
| `customer` | B2B2C variant only — end-customer of the SaaS |
|
|
19
19
|
|
|
20
20
|
## Session Flow
|
|
21
21
|
|
|
22
22
|
1. Request hits `BetterAuthGuard`
|
|
23
23
|
2. Guard checks for `@AllowAnonymous` / `@OptionalAuth` metadata
|
|
24
24
|
3. Calls `auth.api.getSession({ headers })` via Better Auth
|
|
25
|
-
4. Rejects `
|
|
25
|
+
4. Rejects `customer` on non-`@CustomerPortal` routes
|
|
26
26
|
5. Attaches `request.session` and `request.user`
|
|
27
27
|
|
|
28
28
|
## Decorators
|
|
@@ -32,7 +32,7 @@ All routes are protected by default via the global `BetterAuthGuard` (registered
|
|
|
32
32
|
| `@AllowAnonymous()` | Route is fully public — no auth check |
|
|
33
33
|
| `@Public()` | Alias for `@AllowAnonymous()` |
|
|
34
34
|
| `@OptionalAuth()` | Auth checked but not required; `request.user` may be undefined |
|
|
35
|
-
| `@CustomerPortal()` | Allows `
|
|
35
|
+
| `@CustomerPortal()` | Allows `customer` role (B2B2C variant) |
|
|
36
36
|
| `@UserSession()` | Param decorator — injects the `User` from session |
|
|
37
37
|
| `@Session()` | Param decorator — injects full `{ user, session }` object |
|
|
38
38
|
|
|
@@ -58,18 +58,6 @@ Roles: `business_user`, `superadmin`, `customer` (B2B2C variant)
|
|
|
58
58
|
| created_at | timestamp | NO |
|
|
59
59
|
| updated_at | timestamp | NO |
|
|
60
60
|
|
|
61
|
-
#### `oauth_tokens`
|
|
62
|
-
| Column | Type | Nullable | Default |
|
|
63
|
-
|--------|------|----------|---------|
|
|
64
|
-
| id | integer (PK, serial) | NO | nextval |
|
|
65
|
-
| token_type | text | NO | |
|
|
66
|
-
| user_id | integer (FK → users.id) | NO | |
|
|
67
|
-
| access_token | text | NO | |
|
|
68
|
-
| refresh_token | text | NO | |
|
|
69
|
-
| expires_at | timestamp | NO | |
|
|
70
|
-
| created_at | timestamp | NO | CURRENT_TIMESTAMP |
|
|
71
|
-
| updated_at | timestamp | NO | CURRENT_TIMESTAMP |
|
|
72
|
-
|
|
73
61
|
---
|
|
74
62
|
|
|
75
63
|
### Subscriptions
|
|
@@ -20,7 +20,7 @@ Extends Base by adding workspace/project isolation.
|
|
|
20
20
|
|
|
21
21
|
### B2B2C
|
|
22
22
|
Extends Base by adding a separate customer-facing experience (end-users of your customers).
|
|
23
|
-
- Adds `
|
|
23
|
+
- Adds `customer` role
|
|
24
24
|
- Adds `customers-portal` frontend service
|
|
25
25
|
- Adds `@CustomerPortal()` route decorator for customer-only endpoints
|
|
26
26
|
- B2B2C can also be combined with multi-tenancy
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchframe/mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "LaunchFrame MCP Server — knowledge tools for AI agents building LaunchFrame projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"launchframe-mcp": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc && cp -r src/content dist/content",
|
|
10
|
+
"build": "rm -rf dist && tsc && cp -r src/content dist/content",
|
|
11
11
|
"start": "node dist/index.js",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# LaunchFrame Auth Overview
|
|
2
|
-
|
|
3
|
-
## Guard Architecture
|
|
4
|
-
|
|
5
|
-
All routes are protected by default via the global `BetterAuthGuard` (registered in `app.module.ts` as `APP_GUARD`).
|
|
6
|
-
|
|
7
|
-
**Import paths:**
|
|
8
|
-
- Decorators: `src/modules/auth/auth.decorator.ts`
|
|
9
|
-
- Guard: `src/modules/auth/better-auth.guard.ts`
|
|
10
|
-
- Base guard: `src/modules/auth/guards/base-auth.guard.ts`
|
|
11
|
-
|
|
12
|
-
## Roles
|
|
13
|
-
|
|
14
|
-
| Role | Description |
|
|
15
|
-
|------|-------------|
|
|
16
|
-
| `business_user` | Default role for all registered users |
|
|
17
|
-
| `superadmin` | Granted via admin panel; full access |
|
|
18
|
-
| `customer` | B2B2C variant only — end-customer of the SaaS |
|
|
19
|
-
|
|
20
|
-
## Session Flow
|
|
21
|
-
|
|
22
|
-
1. Request hits `BetterAuthGuard`
|
|
23
|
-
2. Guard checks for `@AllowAnonymous` / `@OptionalAuth` metadata
|
|
24
|
-
3. Calls `auth.api.getSession({ headers })` via Better Auth
|
|
25
|
-
4. Rejects `customer` on non-`@CustomerPortal` routes
|
|
26
|
-
5. Attaches `request.session` and `request.user`
|
|
27
|
-
|
|
28
|
-
## Decorators
|
|
29
|
-
|
|
30
|
-
| Decorator | Effect |
|
|
31
|
-
|-----------|--------|
|
|
32
|
-
| `@AllowAnonymous()` | Route is fully public — no auth check |
|
|
33
|
-
| `@Public()` | Alias for `@AllowAnonymous()` |
|
|
34
|
-
| `@OptionalAuth()` | Auth checked but not required; `request.user` may be undefined |
|
|
35
|
-
| `@CustomerPortal()` | Allows `customer` role (B2B2C variant) |
|
|
36
|
-
| `@UserSession()` | Param decorator — injects the `User` from session |
|
|
37
|
-
| `@Session()` | Param decorator — injects full `{ user, session }` object |
|
|
38
|
-
|
|
39
|
-
## Example Usage
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { Controller, Get } from '@nestjs/common';
|
|
43
|
-
import { AllowAnonymous, UserSession } from '../auth/auth.decorator';
|
|
44
|
-
import { User } from '../users/user.entity';
|
|
45
|
-
|
|
46
|
-
@Controller('example')
|
|
47
|
-
export class ExampleController {
|
|
48
|
-
// Public — no auth
|
|
49
|
-
@Get('public')
|
|
50
|
-
@AllowAnonymous()
|
|
51
|
-
getPublic() { ... }
|
|
52
|
-
|
|
53
|
-
// Protected (default) — business_user or superadmin
|
|
54
|
-
@Get('me')
|
|
55
|
-
getMe(@UserSession() user: User) { ... }
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Better Auth Setup
|
|
60
|
-
|
|
61
|
-
Better Auth instance: `src/modules/auth/auth.ts`
|
|
62
|
-
- Adapts to TypeORM via `betterAuthTypeOrmAdapter`
|
|
63
|
-
- Plugins: email/password, Google OAuth, API keys, admin
|
|
64
|
-
- Session stored in `sessions` table; user in `user` table (Better Auth schema)
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Credits Deduction Pattern
|
|
2
|
-
|
|
3
|
-
Add `@DeductCredits(n)` + `@UseGuards(CreditsGuard)` to any route that should cost credits.
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
import { UseGuards } from '@nestjs/common';
|
|
7
|
-
import { CreditsGuard } from '../credits/guards/credits.guard';
|
|
8
|
-
import { DeductCredits } from '../credits/decorators/deduct-credits.decorator';
|
|
9
|
-
|
|
10
|
-
@DeductCredits(10)
|
|
11
|
-
@UseGuards(CreditsGuard)
|
|
12
|
-
@Post('ai-operation')
|
|
13
|
-
async handler(@UserSession() user: User) {
|
|
14
|
-
// CreditsGuard runs before the handler and deducts based on monetization strategy
|
|
15
|
-
// Handler only runs if credits check passes
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Rules
|
|
20
|
-
|
|
21
|
-
- **Both** `@DeductCredits(n)` and `@UseGuards(CreditsGuard)` are required together — neither works alone.
|
|
22
|
-
- `n` is the number of credits to deduct per call.
|
|
23
|
-
- `CreditsGuard` reads the active monetization strategy from `MonetizationConfigService` (cached 24h in Redis).
|
|
24
|
-
- The guard behaviour depends on strategy (see `credits_get_monetization_strategies`).
|
|
25
|
-
- Source files:
|
|
26
|
-
- `src/modules/credits/decorators/deduct-credits.decorator.ts`
|
|
27
|
-
- `src/modules/credits/guards/credits.guard.ts`
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Credits Monetization Strategies
|
|
2
|
-
|
|
3
|
-
The active strategy is stored in `AdminSettings` and cached for 24h in Redis via `MonetizationConfigService`.
|
|
4
|
-
|
|
5
|
-
## Strategies
|
|
6
|
-
|
|
7
|
-
| Strategy | Behaviour |
|
|
8
|
-
|----------|-----------|
|
|
9
|
-
| `free` | All credit checks bypass — no deduction ever |
|
|
10
|
-
| `subscription` | Credit checks bypass — use feature gates to restrict instead |
|
|
11
|
-
| `credits` | Deduct `n` credits from user balance; 402 if insufficient |
|
|
12
|
-
| `hybrid` | Deduct from `subscription.plan.monthlyCredits` allowance first; bill Polar overage when exhausted |
|
|
13
|
-
|
|
14
|
-
## Hybrid details
|
|
15
|
-
|
|
16
|
-
- Monthly allowance: `subscription.plan.monthlyCredits` (column on `SubscriptionPlan` entity, NOT a feature code)
|
|
17
|
-
- Overage rate: `subscription.plan.overageRate` (column on `SubscriptionPlan` entity)
|
|
18
|
-
- Overage is charged via Polar meter API
|
|
19
|
-
|
|
20
|
-
## Choosing a strategy
|
|
21
|
-
|
|
22
|
-
- **SaaS with flat plans** → `subscription` (gates control feature access, no per-call cost)
|
|
23
|
-
- **API / AI product** → `credits` (pay-as-you-go balance)
|
|
24
|
-
- **AI + subscription tiers** → `hybrid` (included monthly quota + Polar overage billing)
|
|
25
|
-
- **Early stage / free tier** → `free` (disable all metering)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Cron Jobs — LaunchFrame Pattern
|
|
2
|
-
|
|
3
|
-
## Key Rules
|
|
4
|
-
|
|
5
|
-
- **Single service**: All cron jobs live in `src/jobs/cron.service.ts`. Do NOT create new cron services unless the job is genuinely complex enough to warrant its own service.
|
|
6
|
-
- **Lightweight methods**: Cron methods should enqueue work to Bull and return. Never do heavy processing inline.
|
|
7
|
-
- `ScheduleModule.forRoot()` is already registered in `app.module.ts` — do not add it again.
|
|
8
|
-
|
|
9
|
-
## Imports
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
13
|
-
import { InjectQueue } from '@nestjs/bull';
|
|
14
|
-
import { Queue } from 'bull';
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Available CronExpression values
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
CronExpression.EVERY_MINUTE
|
|
21
|
-
CronExpression.EVERY_30_MINUTES
|
|
22
|
-
CronExpression.EVERY_HOUR
|
|
23
|
-
CronExpression.EVERY_DAY_AT_MIDNIGHT
|
|
24
|
-
CronExpression.EVERY_WEEK
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Use a raw cron string (e.g. `'0 9 * * 1-5'`) if no preset fits.
|
|
28
|
-
|
|
29
|
-
## Example method
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
@Cron(CronExpression.EVERY_HOUR)
|
|
33
|
-
async processWebhooks() {
|
|
34
|
-
this.logger.log('Starting webhook processing job...');
|
|
35
|
-
try {
|
|
36
|
-
const items = await this.repo.find({ where: { processed: false } });
|
|
37
|
-
for (const item of items) {
|
|
38
|
-
await this.queue.add({ id: item.id }, {
|
|
39
|
-
attempts: 3,
|
|
40
|
-
backoff: { type: 'exponential', delay: 2000 },
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
} catch (error) {
|
|
44
|
-
this.logger.error('Error in processWebhooks:', error);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Module registration
|
|
50
|
-
|
|
51
|
-
`CronService` is already a provider in `JobsModule`. If you inject a new queue or repository, add the corresponding `BullModule.registerQueue` / `TypeOrmModule.forFeature` to `JobsModule` imports.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# TypeORM Entity Conventions in LaunchFrame
|
|
2
|
-
|
|
3
|
-
## Required Decorators
|
|
4
|
-
|
|
5
|
-
Every entity must have these four:
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
@Entity('snake_case_table_name')
|
|
9
|
-
export class MyEntity {
|
|
10
|
-
@PrimaryGeneratedColumn()
|
|
11
|
-
id: number;
|
|
12
|
-
|
|
13
|
-
// ... domain columns ...
|
|
14
|
-
|
|
15
|
-
@CreateDateColumn()
|
|
16
|
-
createdAt: Date;
|
|
17
|
-
|
|
18
|
-
@UpdateDateColumn()
|
|
19
|
-
updatedAt: Date;
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Naming Strategy
|
|
24
|
-
|
|
25
|
-
A global `SnakeNamingStrategy` is configured in TypeORM. This means:
|
|
26
|
-
- `createdAt` → `created_at` automatically
|
|
27
|
-
- `userId` → `user_id` automatically
|
|
28
|
-
- Do NOT add `name: 'column_name'` unless you need to deviate from the convention.
|
|
29
|
-
|
|
30
|
-
Exception: some legacy entities specify explicit `name:` — that is acceptable but not required for new entities.
|
|
31
|
-
|
|
32
|
-
## Column Types
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
// String
|
|
36
|
-
@Column() name: string;
|
|
37
|
-
@Column({ type: 'text', nullable: true }) description: string;
|
|
38
|
-
@Column({ type: 'varchar', length: 255, nullable: true }) slug: string;
|
|
39
|
-
|
|
40
|
-
// Number
|
|
41
|
-
@Column({ type: 'int' }) count: number;
|
|
42
|
-
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true }) amount: number;
|
|
43
|
-
|
|
44
|
-
// Boolean
|
|
45
|
-
@Column({ default: false }) isActive: boolean;
|
|
46
|
-
|
|
47
|
-
// JSON
|
|
48
|
-
@Column({ type: 'jsonb' }) metadata: Record<string, any>;
|
|
49
|
-
@Column({ type: 'jsonb', nullable: true }) options: Record<string, any>;
|
|
50
|
-
|
|
51
|
-
// Enum (define enum in same file)
|
|
52
|
-
export enum MyStatus { ACTIVE = 'active', INACTIVE = 'inactive' }
|
|
53
|
-
@Column({ type: 'enum', enum: MyStatus }) status: MyStatus;
|
|
54
|
-
|
|
55
|
-
// UUID primary key
|
|
56
|
-
@PrimaryGeneratedColumn('uuid') id: string;
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Relations
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
// Foreign key column + relation (preferred pattern)
|
|
63
|
-
@Column({ type: 'int' })
|
|
64
|
-
userId: number;
|
|
65
|
-
|
|
66
|
-
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
|
67
|
-
user: User;
|
|
68
|
-
|
|
69
|
-
// One-to-many
|
|
70
|
-
@OneToMany(() => CreditTransaction, (tx) => tx.user)
|
|
71
|
-
transactions: CreditTransaction[];
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Multi-Tenancy
|
|
75
|
-
|
|
76
|
-
For multi-tenant variants, add `projectId` to every domain entity:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// MULTI_TENANT_FIELDS_START
|
|
80
|
-
@Column() projectId: number;
|
|
81
|
-
// MULTI_TENANT_FIELDS_END
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Leave the section markers in place — the CLI uses them to splice in multi-tenant fields.
|
|
85
|
-
|
|
86
|
-
## File Location
|
|
87
|
-
|
|
88
|
-
```
|
|
89
|
-
src/modules/<domain>/entities/<entity-name>.entity.ts
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Register in the module via `TypeOrmModule.forFeature([MyEntity])`.
|
|
93
|
-
|
|
94
|
-
## Migration
|
|
95
|
-
|
|
96
|
-
Migrations live in `src/migrations/`. Name format: `<timestamp>-<PascalCaseDescription>.ts`.
|
|
97
|
-
Run inside the backend container: `npm run migration:run`.
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# Environment Variable Conventions
|
|
2
|
-
|
|
3
|
-
## Single Centralized `.env`
|
|
4
|
-
|
|
5
|
-
All environment variables live in **one file**: `infrastructure/.env`
|
|
6
|
-
|
|
7
|
-
Docker Compose mounts it into every container via `env_file:` — **never create per-service `.env` files**.
|
|
8
|
-
|
|
9
|
-
## Variable Naming
|
|
10
|
-
|
|
11
|
-
Variables are **canonical** (no service-specific prefixes like `REACT_APP_`, `VITE_`, or `NEXT_PUBLIC_`).
|
|
12
|
-
The `docker-compose.yml` files map canonical names to the appropriate prefixed versions for each service.
|
|
13
|
-
|
|
14
|
-
Example mapping in docker-compose:
|
|
15
|
-
```yaml
|
|
16
|
-
environment:
|
|
17
|
-
- VITE_API_BASE_URL=${API_BASE_URL}
|
|
18
|
-
- NEXT_PUBLIC_PRIMARY_DOMAIN=${PRIMARY_DOMAIN}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Key Variables Reference
|
|
22
|
-
|
|
23
|
-
### Application
|
|
24
|
-
```
|
|
25
|
-
NODE_ENV=production
|
|
26
|
-
APP_NAME=
|
|
27
|
-
PRIMARY_DOMAIN=
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Database (PostgreSQL)
|
|
31
|
-
```
|
|
32
|
-
DB_HOST=database
|
|
33
|
-
DB_PORT=5432
|
|
34
|
-
DB_USERNAME=postgres
|
|
35
|
-
DB_PASSWORD=
|
|
36
|
-
DB_DATABASE=
|
|
37
|
-
DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Redis
|
|
41
|
-
```
|
|
42
|
-
REDIS_HOST=redis
|
|
43
|
-
REDIS_PORT=6379
|
|
44
|
-
REDIS_PASSWORD=
|
|
45
|
-
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Service URLs & Ports
|
|
49
|
-
```
|
|
50
|
-
BACKEND_PORT=4000
|
|
51
|
-
API_BASE_URL=http://localhost:4000
|
|
52
|
-
BACKEND_API_URL=http://backend:4000
|
|
53
|
-
|
|
54
|
-
ADMIN_FRONTEND_PORT=3001
|
|
55
|
-
ADMIN_BASE_URL=http://localhost:3001
|
|
56
|
-
|
|
57
|
-
CUSTOMERS_FRONTEND_PORT=3000
|
|
58
|
-
FRONTEND_BASE_URL=http://localhost:3000
|
|
59
|
-
|
|
60
|
-
WEBSITE_PORT=8080
|
|
61
|
-
WEBSITE_BASE_URL=http://localhost:8080
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Authentication & Security
|
|
65
|
-
```
|
|
66
|
-
BETTER_AUTH_SECRET= # min 32 chars, generate: openssl rand -base64 32
|
|
67
|
-
INITIAL_CREDITS=100 # credits granted to new users
|
|
68
|
-
BULL_ADMIN_TOKEN= # Bull Board dashboard access
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Email
|
|
72
|
-
```
|
|
73
|
-
RESEND_API_KEY= # production email (Resend)
|
|
74
|
-
MAIL_HOST= # SMTP host (dev: Mailtrap)
|
|
75
|
-
MAIL_USER=
|
|
76
|
-
MAIL_PASSWORD=
|
|
77
|
-
MAIL_FROM=noreply@${PRIMARY_DOMAIN}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Payments (Polar.sh)
|
|
81
|
-
```
|
|
82
|
-
POLAR_ACCESS_TOKEN=
|
|
83
|
-
POLAR_SUCCESS_URL=${ADMIN_BASE_URL}/payments/success?checkout_id={CHECKOUT_ID}
|
|
84
|
-
POLAR_WEBHOOK_SECRET=
|
|
85
|
-
# POLAR_ENVIRONMENT=sandbox # defaults to 'sandbox' in dev, 'production' in prod
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Google
|
|
89
|
-
```
|
|
90
|
-
GOOGLE_CLIENT_ID=
|
|
91
|
-
GOOGLE_CLIENT_SECRET=
|
|
92
|
-
GOOGLE_REDIRECT_URI=http://localhost:4000/auth/google/callback
|
|
93
|
-
GOOGLE_ANALYTICS_ID=
|
|
94
|
-
GOOGLE_CLOUD_STORAGE_BUCKET=
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Monitoring
|
|
98
|
-
```
|
|
99
|
-
BACKEND_SENTRY_DSN=
|
|
100
|
-
ADMIN_FRONTEND_SENTRY_DSN=
|
|
101
|
-
CUSTOMERS_PORTAL_SENTRY_DSN=
|
|
102
|
-
MIXPANEL_PROJECT_TOKEN=
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Real-time
|
|
106
|
-
```
|
|
107
|
-
PUSHER_KEY=
|
|
108
|
-
PUSHER_CLUSTER=
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Adding a New Variable
|
|
112
|
-
|
|
113
|
-
1. Add it to `infrastructure/base/.env.example` with a comment
|
|
114
|
-
2. Add it to `infrastructure/.env` (your local copy, never committed)
|
|
115
|
-
3. Reference it in the relevant `docker-compose.yml` `environment:` section if a prefix is needed
|
|
116
|
-
4. Access in NestJS via `process.env.VAR_NAME` or via `ConfigService`
|
|
117
|
-
|
|
118
|
-
## Rules
|
|
119
|
-
|
|
120
|
-
- **NEVER** commit `.env` — it is gitignored
|
|
121
|
-
- **NEVER** create per-service `.env` files
|
|
122
|
-
- All secrets (auth, DB, API keys) go in `infrastructure/.env` only
|
|
123
|
-
- Use `base/.env.example` as the template — it has all variables with placeholder values
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# Feature Gate System Overview
|
|
2
|
-
|
|
3
|
-
## Architecture
|
|
4
|
-
|
|
5
|
-
Features are fully generic and defined in the database — there are no hardcoded feature codes.
|
|
6
|
-
|
|
7
|
-
| Table | Purpose |
|
|
8
|
-
|-------|---------|
|
|
9
|
-
| `subscription_plan_features` | Feature definitions: `code`, `name`, `featureType` (boolean/numeric/text), `defaultValue` |
|
|
10
|
-
| `subscription_plan_feature_values` | Per-plan feature values: links plan → feature → value (JSONB) |
|
|
11
|
-
|
|
12
|
-
Features are created and managed via the admin portal. The template ships with a single `basic_access` feature for the free plan as a starting point.
|
|
13
|
-
|
|
14
|
-
## Querying Features
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import { UserSubscriptionService } from '../subscriptions/services/user-subscription.service';
|
|
18
|
-
|
|
19
|
-
// Returns Record<string, any> — keys are feature codes, values are the raw stored value
|
|
20
|
-
const features = await this.userSubscriptionService.getCurrentFeatures(userId);
|
|
21
|
-
|
|
22
|
-
// Example: check a boolean feature
|
|
23
|
-
const hasFeature = features['your_feature_code'] === true;
|
|
24
|
-
|
|
25
|
-
// Example: check a numeric feature (-1 means unlimited)
|
|
26
|
-
const limit = features['your_feature_code'] as number ?? 0;
|
|
27
|
-
const isUnlimited = limit === -1;
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Check Pattern (no decorator — manual check only)
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { Injectable, ForbiddenException } from '@nestjs/common';
|
|
34
|
-
import { UserSubscriptionService } from '../subscriptions/services/user-subscription.service';
|
|
35
|
-
|
|
36
|
-
@Injectable()
|
|
37
|
-
export class YourService {
|
|
38
|
-
constructor(
|
|
39
|
-
private readonly userSubscriptionService: UserSubscriptionService,
|
|
40
|
-
) {}
|
|
41
|
-
|
|
42
|
-
async guardedOperation(userId: number) {
|
|
43
|
-
const features = await this.userSubscriptionService.getCurrentFeatures(userId);
|
|
44
|
-
|
|
45
|
-
// Boolean feature
|
|
46
|
-
if (!features['your_boolean_feature']) {
|
|
47
|
-
throw new ForbiddenException('Your plan does not include this feature');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Numeric feature with limit
|
|
51
|
-
const limit = features['your_numeric_feature'] as number ?? 0;
|
|
52
|
-
const currentCount = await this.getCount(userId);
|
|
53
|
-
if (limit !== -1 && currentCount >= limit) {
|
|
54
|
-
throw new ForbiddenException(`Plan limit reached (${limit})`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Unlimited Sentinel
|
|
61
|
-
|
|
62
|
-
For numeric features, `-1` means unlimited. Always check for it:
|
|
63
|
-
```typescript
|
|
64
|
-
const isUnlimited = limit === -1;
|
|
65
|
-
if (!isUnlimited && currentCount >= limit) { ... }
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Credits-specific Plan Fields
|
|
69
|
-
|
|
70
|
-
Monthly credits and overage rate are NOT generic features — they live as dedicated columns on `SubscriptionPlan`:
|
|
71
|
-
- `plan.monthlyCredits: number | null` — monthly credit allowance (null = not applicable)
|
|
72
|
-
- `plan.overageRate: number | null` — per-credit overage cost (null = no overage)
|
|
73
|
-
|
|
74
|
-
These are read directly from the subscription, not via `getCurrentFeatures()`.
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# NestJS Module Structure in LaunchFrame
|
|
2
|
-
|
|
3
|
-
## Folder Layout
|
|
4
|
-
|
|
5
|
-
Every domain lives under `src/modules/<domain>/`:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
src/modules/<domain>/
|
|
9
|
-
├── <domain>.module.ts # Module definition
|
|
10
|
-
├── <domain>.service.ts # Business logic
|
|
11
|
-
├── <domain>.controller.ts # HTTP routes (if applicable)
|
|
12
|
-
└── entities/
|
|
13
|
-
└── <entity>.entity.ts # TypeORM entity
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Module Rules
|
|
17
|
-
|
|
18
|
-
1. **Entity registration**: Use `TypeOrmModule.forFeature([...entities])` inside `imports`.
|
|
19
|
-
2. **Circular deps**: Wrap with `forwardRef(() => OtherModule)` on both sides.
|
|
20
|
-
3. **Bull queues**: Import `BullQueueModule` (shared forRoot) + `BullModule.registerQueue({ name })` — never re-declare `forRoot`.
|
|
21
|
-
4. **Exports**: Only export what other modules actually need (service, guard, etc.).
|
|
22
|
-
5. **Registering in AppModule**: Add your module to `app.module.ts` imports array.
|
|
23
|
-
|
|
24
|
-
## Variant Section Markers
|
|
25
|
-
|
|
26
|
-
Variant-specific code uses comment markers so the CLI can splice in additions:
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
// MULTI_TENANT_IMPORTS_START
|
|
30
|
-
// MULTI_TENANT_IMPORTS_END
|
|
31
|
-
|
|
32
|
-
// MULTI_TENANT_ENTITIES_START
|
|
33
|
-
// MULTI_TENANT_ENTITIES_END
|
|
34
|
-
|
|
35
|
-
// MULTI_TENANT_PROVIDERS_START
|
|
36
|
-
// MULTI_TENANT_PROVIDERS_END
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Leave these markers in place even if empty — the CLI relies on them.
|
|
40
|
-
|
|
41
|
-
## Placement in AppModule
|
|
42
|
-
|
|
43
|
-
`app.module.ts` already bootstraps global modules (TypeORM, Bull, Schedule, etc.).
|
|
44
|
-
Your module only needs to import what it directly depends on.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Bull Queues
|
|
2
|
-
|
|
3
|
-
LaunchFrame registers 5 Bull queues via `BullQueueModule` (`src/modules/bull/bull.module.ts`).
|
|
4
|
-
|
|
5
|
-
| Queue name | Purpose |
|
|
6
|
-
|--------------|----------------------------------------------|
|
|
7
|
-
| `emails` | Transactional email sending via Resend |
|
|
8
|
-
| `api` | Outbound HTTP API calls to third-party services |
|
|
9
|
-
| `webhooks` | Processing inbound webhook payloads |
|
|
10
|
-
|
|
11
|
-
## Usage rules
|
|
12
|
-
|
|
13
|
-
- **Import `BullQueueModule`** (not `BullModule.forRoot`) in your feature module — it re-uses the shared Redis connection.
|
|
14
|
-
- Register the queue in your module with `BullModule.registerQueue({ name: 'queue-name' })`, imported from `bull.module.ts`.
|
|
15
|
-
- Inject with `@InjectQueue('queue-name') private queue: Queue` from `@nestjs/bull`.
|
|
16
|
-
- Default job options: `{ attempts: 3, backoff: { type: 'exponential', delay: 2000 } }`.
|
|
17
|
-
- **Always `throw error`** in processor catch blocks — Bull needs the error to trigger retries.
|
|
18
|
-
- Keep cron/scheduler methods lightweight: enqueue to Bull, do heavy work in the processor.
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# LaunchFrame Variants
|
|
2
|
-
|
|
3
|
-
LaunchFrame generates projects in one of three variants. Each variant is a superset of the previous.
|
|
4
|
-
|
|
5
|
-
## Variant Overview
|
|
6
|
-
|
|
7
|
-
### Base (B2B, single-tenant)
|
|
8
|
-
The simplest variant. One admin panel, one customer-facing portal, no multi-tenancy.
|
|
9
|
-
- Single workspace per account
|
|
10
|
-
- No `projectId` on entities
|
|
11
|
-
- No `projects` module
|
|
12
|
-
- Roles: `admin`, `user`
|
|
13
|
-
|
|
14
|
-
### Multi-tenant
|
|
15
|
-
Extends Base by adding workspace/project isolation.
|
|
16
|
-
- `projects` module: CRUD, ownership guards
|
|
17
|
-
- `projectId: number` column on all domain entities
|
|
18
|
-
- Project ownership guard on all project-scoped routes
|
|
19
|
-
- Roles: `admin`, `user` (scoped to project)
|
|
20
|
-
|
|
21
|
-
### B2B2C
|
|
22
|
-
Extends Base by adding a separate customer-facing experience (end-users of your customers).
|
|
23
|
-
- Adds `customer` role
|
|
24
|
-
- Adds `customers-portal` frontend service
|
|
25
|
-
- Adds `@CustomerPortal()` route decorator for customer-only endpoints
|
|
26
|
-
- B2B2C can also be combined with multi-tenancy
|
|
27
|
-
|
|
28
|
-
## Section Marker Syntax
|
|
29
|
-
|
|
30
|
-
Source files use markers so the CLI can strip/inject variant-specific code blocks:
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
// {{SECTION_NAME}}_START
|
|
34
|
-
... variant-specific code ...
|
|
35
|
-
// {{SECTION_NAME}}_END
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Common section names:
|
|
39
|
-
- `MULTI_TENANT_FIELDS` — `projectId` columns on entities
|
|
40
|
-
- `MULTI_TENANT_GUARD` — project ownership guard imports/decorators
|
|
41
|
-
- `CUSTOMER_PORTAL_ROUTES` — B2B2C customer route blocks
|
|
42
|
-
|
|
43
|
-
The CLI strips sections that don't apply to the chosen variant and removes the markers from kept sections.
|
|
44
|
-
|
|
45
|
-
## Coding Guidelines per Variant
|
|
46
|
-
|
|
47
|
-
### When writing entities
|
|
48
|
-
- **Base**: no `projectId`
|
|
49
|
-
- **Multi-tenant**: add `@Column() projectId: number;` wrapped in `// MULTI_TENANT_FIELDS_START` / `_END`
|
|
50
|
-
|
|
51
|
-
### When writing controllers
|
|
52
|
-
- **Multi-tenant**: add project ownership guard
|
|
53
|
-
```typescript
|
|
54
|
-
// MULTI_TENANT_GUARD_START
|
|
55
|
-
@UseGuards(ProjectOwnershipGuard)
|
|
56
|
-
// MULTI_TENANT_GUARD_END
|
|
57
|
-
```
|
|
58
|
-
- **B2B2C**: wrap customer-only routes with `@CustomerPortal()` and section markers
|
|
59
|
-
|
|
60
|
-
### When scaffolding modules
|
|
61
|
-
- Use section markers for any variant-specific imports, providers, or route decorators
|
|
62
|
-
- Keep the base code path clean — markers are additive
|
|
63
|
-
|
|
64
|
-
## Which Variant to Target
|
|
65
|
-
|
|
66
|
-
When writing code for a LaunchFrame project, check the project's `VARIANT` env var or ask the user.
|
|
67
|
-
Default assumption: **Base** (single-tenant B2B) unless told otherwise.
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Webhook Architecture
|
|
2
|
-
|
|
3
|
-
LaunchFrame uses a **receipt/processing separation** pattern for all inbound webhooks.
|
|
4
|
-
|
|
5
|
-
## Flow
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Webhook Provider → Controller (receipt) → WebhookLog (DB) → Bull queue → Processor (processing)
|
|
9
|
-
↑
|
|
10
|
-
Cron retries failed jobs
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## 1. Controller (Receipt Layer)
|
|
14
|
-
|
|
15
|
-
The controller saves the raw payload and returns 200 **immediately** — never process inline.
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
import { Controller, Post, Req, Res } from '@nestjs/common';
|
|
19
|
-
import { Public } from '../auth/auth.decorator';
|
|
20
|
-
import { UseGuards } from '@nestjs/common';
|
|
21
|
-
import { PolarWebhookGuard } from './guards/polar-webhook.guard';
|
|
22
|
-
|
|
23
|
-
@Controller('webhooks')
|
|
24
|
-
export class WebhooksController {
|
|
25
|
-
@Post('polar')
|
|
26
|
-
@Public()
|
|
27
|
-
@UseGuards(PolarWebhookGuard)
|
|
28
|
-
async handlePolar(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
29
|
-
// Save raw payload to WebhookLog, enqueue to 'webhooks' Bull queue
|
|
30
|
-
res.status(200).send(); // Always return 200 immediately
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## 2. WebhookLog Entity
|
|
36
|
-
|
|
37
|
-
Fields: `provider` (enum: POLAR | PAYPAL | STRIPE), `eventType`, `webhookId`, `payload` (jsonb),
|
|
38
|
-
`headers` (jsonb), `processed` (bool, default false), `retryCount` (int, default 0), `processingError`.
|
|
39
|
-
|
|
40
|
-
## 3. Processor (Processing Layer)
|
|
41
|
-
|
|
42
|
-
A Bull processor on the `webhooks` queue handles the actual logic. Always `throw error` in catch blocks.
|
|
43
|
-
|
|
44
|
-
## 4. Retry Cron
|
|
45
|
-
|
|
46
|
-
`CronService` runs `EVERY_HOUR`, fetches up to 100 `WebhookLog` records where `processed=false AND retryCount < 5`,
|
|
47
|
-
and re-enqueues them to the `webhooks` Bull queue (which has its own 3-attempt exponential backoff).
|
|
48
|
-
App-level max retries: **5** (enforced by the cron filter).
|
|
49
|
-
|
|
50
|
-
## Guards
|
|
51
|
-
|
|
52
|
-
- `PolarWebhookGuard` — validates Polar webhook signature
|
|
53
|
-
- Add equivalent guards for other providers (PayPal, Stripe) as needed
|