@l4yercak3/cli 1.2.15 → 1.2.18
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/docs/INTEGRATION_PATHS_ARCHITECTURE.md +1543 -0
- package/package.json +1 -1
- package/src/commands/spread.js +101 -6
- package/src/detectors/database-detector.js +245 -0
- package/src/detectors/index.js +17 -4
- package/src/generators/api-only/client.js +683 -0
- package/src/generators/api-only/index.js +96 -0
- package/src/generators/api-only/types.js +618 -0
- package/src/generators/api-only/webhooks.js +377 -0
- package/src/generators/index.js +88 -2
- package/src/generators/mcp-guide-generator.js +256 -0
- package/src/generators/quickstart/components/index.js +1699 -0
- package/src/generators/quickstart/database/convex.js +1257 -0
- package/src/generators/quickstart/database/index.js +34 -0
- package/src/generators/quickstart/database/supabase.js +1132 -0
- package/src/generators/quickstart/hooks/index.js +1047 -0
- package/src/generators/quickstart/index.js +151 -0
- package/src/generators/quickstart/pages/index.js +1466 -0
- package/src/mcp/registry/domains/applications.js +4 -4
- package/src/mcp/registry/domains/benefits.js +798 -0
- package/src/mcp/registry/domains/crm.js +11 -11
- package/src/mcp/registry/domains/events.js +12 -12
- package/src/mcp/registry/domains/forms.js +12 -12
- package/src/mcp/registry/index.js +2 -0
- package/tests/database-detector.test.js +221 -0
- package/tests/generators-index.test.js +215 -3
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
# L4YERCAK3 CLI Integration Paths Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When a developer runs `l4yercak3 spread`, they go through a comprehensive setup flow that results in a fully integrated application. This document outlines the three integration paths and what each generates.
|
|
6
|
+
|
|
7
|
+
## Flow Diagram
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
l4yercak3 spread
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
┌─────────────────────────────────────┐
|
|
14
|
+
│ 1. DETECT PROJECT │
|
|
15
|
+
│ - Framework (Next.js, Expo, etc.) │
|
|
16
|
+
│ - TypeScript/JavaScript │
|
|
17
|
+
│ - Router type (App/Pages) │
|
|
18
|
+
│ - Existing database (detect) │
|
|
19
|
+
└─────────────────────────────────────┘
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────────────────────────┐
|
|
23
|
+
│ 2. SELECT FEATURES │
|
|
24
|
+
│ ◉ CRM (contacts, organizations) │
|
|
25
|
+
│ ◉ Events (ticketing, check-in) │
|
|
26
|
+
│ ◉ Forms (builder, submissions) │
|
|
27
|
+
│ ◉ Products (catalog, inventory) │
|
|
28
|
+
│ ◉ Checkout (cart, payments) │
|
|
29
|
+
│ ◉ Invoicing (B2B/B2C) │
|
|
30
|
+
│ ◉ Benefits (claims, commissions) │
|
|
31
|
+
│ ◉ Certificates (CME, attendance) │
|
|
32
|
+
│ ◉ Projects (task management) │
|
|
33
|
+
│ ◉ Authentication (OAuth providers) │
|
|
34
|
+
└─────────────────────────────────────┘
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────────────────────────┐
|
|
38
|
+
│ 3. CHOOSE INTEGRATION PATH │
|
|
39
|
+
│ │
|
|
40
|
+
│ ◉ Quick Start (Recommended) │
|
|
41
|
+
│ Full-stack with UI components │
|
|
42
|
+
│ │
|
|
43
|
+
│ ○ API Only │
|
|
44
|
+
│ Just the typed API client │
|
|
45
|
+
│ │
|
|
46
|
+
│ ○ MCP-Assisted │
|
|
47
|
+
│ AI-powered custom generation │
|
|
48
|
+
└─────────────────────────────────────┘
|
|
49
|
+
│
|
|
50
|
+
▼ (if Quick Start or no DB detected)
|
|
51
|
+
┌─────────────────────────────────────┐
|
|
52
|
+
│ 4. DATABASE SELECTION │
|
|
53
|
+
│ (only if no existing DB detected) │
|
|
54
|
+
│ │
|
|
55
|
+
│ ◉ Convex (Recommended) │
|
|
56
|
+
│ Real-time, serverless │
|
|
57
|
+
│ │
|
|
58
|
+
│ ○ Supabase │
|
|
59
|
+
│ PostgreSQL + Auth + Storage │
|
|
60
|
+
│ │
|
|
61
|
+
│ ○ None / Existing │
|
|
62
|
+
│ Skip database setup │
|
|
63
|
+
└─────────────────────────────────────┘
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌─────────────────────────────────────┐
|
|
67
|
+
│ 5. GENERATE & CONFIGURE │
|
|
68
|
+
│ - Create files based on path │
|
|
69
|
+
│ - Set up database (if selected) │
|
|
70
|
+
│ - Configure MCP server │
|
|
71
|
+
│ - Register with L4YERCAK3 backend │
|
|
72
|
+
└─────────────────────────────────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Integration Path 1: Quick Start (Full Stack)
|
|
78
|
+
|
|
79
|
+
**Target User:** Developers who want a working app fast with best practices baked in.
|
|
80
|
+
|
|
81
|
+
### What Gets Generated
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
project/
|
|
85
|
+
├── .env.local # API keys, DB connection
|
|
86
|
+
├── l4yercak3.config.ts # L4YERCAK3 configuration
|
|
87
|
+
│
|
|
88
|
+
├── lib/
|
|
89
|
+
│ ├── l4yercak3/
|
|
90
|
+
│ │ ├── client.ts # API client (typed)
|
|
91
|
+
│ │ ├── types.ts # All TypeScript types
|
|
92
|
+
│ │ ├── hooks/ # React Query hooks
|
|
93
|
+
│ │ │ ├── use-contacts.ts
|
|
94
|
+
│ │ │ ├── use-events.ts
|
|
95
|
+
│ │ │ ├── use-forms.ts
|
|
96
|
+
│ │ │ ├── use-products.ts
|
|
97
|
+
│ │ │ ├── use-checkout.ts
|
|
98
|
+
│ │ │ └── ...
|
|
99
|
+
│ │ └── utils.ts # Helper functions
|
|
100
|
+
│ │
|
|
101
|
+
│ └── db/ # Database layer
|
|
102
|
+
│ ├── convex/ # If Convex selected
|
|
103
|
+
│ │ ├── schema.ts # Convex schema
|
|
104
|
+
│ │ ├── contacts.ts # Contact queries/mutations
|
|
105
|
+
│ │ ├── events.ts
|
|
106
|
+
│ │ └── ...
|
|
107
|
+
│ └── supabase/ # If Supabase selected
|
|
108
|
+
│ ├── schema.sql # PostgreSQL schema
|
|
109
|
+
│ ├── migrations/
|
|
110
|
+
│ └── client.ts
|
|
111
|
+
│
|
|
112
|
+
├── components/
|
|
113
|
+
│ └── l4yercak3/
|
|
114
|
+
│ ├── crm/
|
|
115
|
+
│ │ ├── ContactList.tsx
|
|
116
|
+
│ │ ├── ContactCard.tsx
|
|
117
|
+
│ │ ├── ContactForm.tsx
|
|
118
|
+
│ │ ├── ContactDetail.tsx
|
|
119
|
+
│ │ └── OrganizationList.tsx
|
|
120
|
+
│ ├── events/
|
|
121
|
+
│ │ ├── EventList.tsx
|
|
122
|
+
│ │ ├── EventCard.tsx
|
|
123
|
+
│ │ ├── EventDetail.tsx
|
|
124
|
+
│ │ ├── TicketSelector.tsx
|
|
125
|
+
│ │ └── CheckInScanner.tsx
|
|
126
|
+
│ ├── forms/
|
|
127
|
+
│ │ ├── FormRenderer.tsx
|
|
128
|
+
│ │ ├── FormBuilder.tsx # (if admin features enabled)
|
|
129
|
+
│ │ └── FormSubmissions.tsx
|
|
130
|
+
│ ├── checkout/
|
|
131
|
+
│ │ ├── Cart.tsx
|
|
132
|
+
│ │ ├── CheckoutForm.tsx
|
|
133
|
+
│ │ └── OrderConfirmation.tsx
|
|
134
|
+
│ ├── invoicing/
|
|
135
|
+
│ │ ├── InvoiceList.tsx
|
|
136
|
+
│ │ ├── InvoiceDetail.tsx
|
|
137
|
+
│ │ └── InvoicePDF.tsx
|
|
138
|
+
│ ├── benefits/
|
|
139
|
+
│ │ ├── ClaimsList.tsx
|
|
140
|
+
│ │ ├── ClaimForm.tsx
|
|
141
|
+
│ │ └── WalletManager.tsx
|
|
142
|
+
│ └── shared/
|
|
143
|
+
│ ├── LoadingSpinner.tsx
|
|
144
|
+
│ ├── ErrorBoundary.tsx
|
|
145
|
+
│ └── Pagination.tsx
|
|
146
|
+
│
|
|
147
|
+
├── app/ # Next.js App Router
|
|
148
|
+
│ ├── api/
|
|
149
|
+
│ │ └── l4yercak3/
|
|
150
|
+
│ │ └── [...path]/
|
|
151
|
+
│ │ └── route.ts # API proxy route
|
|
152
|
+
│ │
|
|
153
|
+
│ ├── (auth)/
|
|
154
|
+
│ │ ├── login/page.tsx
|
|
155
|
+
│ │ └── callback/page.tsx
|
|
156
|
+
│ │
|
|
157
|
+
│ ├── crm/
|
|
158
|
+
│ │ ├── page.tsx # Contact list
|
|
159
|
+
│ │ └── [id]/page.tsx # Contact detail
|
|
160
|
+
│ │
|
|
161
|
+
│ ├── events/
|
|
162
|
+
│ │ ├── page.tsx # Event listing
|
|
163
|
+
│ │ ├── [id]/page.tsx # Event detail
|
|
164
|
+
│ │ └── [id]/register/page.tsx # Registration form
|
|
165
|
+
│ │
|
|
166
|
+
│ ├── checkout/
|
|
167
|
+
│ │ ├── page.tsx # Cart/checkout
|
|
168
|
+
│ │ └── success/page.tsx # Order confirmation
|
|
169
|
+
│ │
|
|
170
|
+
│ └── admin/ # (if admin features)
|
|
171
|
+
│ ├── events/page.tsx
|
|
172
|
+
│ ├── forms/page.tsx
|
|
173
|
+
│ └── invoices/page.tsx
|
|
174
|
+
│
|
|
175
|
+
└── convex/ # If Convex selected
|
|
176
|
+
├── _generated/
|
|
177
|
+
├── schema.ts
|
|
178
|
+
├── contacts.ts
|
|
179
|
+
├── events.ts
|
|
180
|
+
├── forms.ts
|
|
181
|
+
├── products.ts
|
|
182
|
+
├── orders.ts
|
|
183
|
+
└── sync.ts # L4YERCAK3 sync logic
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Database Schema (Convex Example)
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// convex/schema.ts
|
|
190
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
191
|
+
import { v } from "convex/values";
|
|
192
|
+
|
|
193
|
+
export default defineSchema({
|
|
194
|
+
// Local cache of L4YERCAK3 contacts
|
|
195
|
+
contacts: defineTable({
|
|
196
|
+
l4yercak3Id: v.string(), // ID from L4YERCAK3 backend
|
|
197
|
+
firstName: v.string(),
|
|
198
|
+
lastName: v.string(),
|
|
199
|
+
email: v.string(),
|
|
200
|
+
phone: v.optional(v.string()),
|
|
201
|
+
company: v.optional(v.string()),
|
|
202
|
+
status: v.string(),
|
|
203
|
+
tags: v.array(v.string()),
|
|
204
|
+
syncedAt: v.number(),
|
|
205
|
+
localOnly: v.boolean(), // Not yet synced to L4YERCAK3
|
|
206
|
+
})
|
|
207
|
+
.index("by_l4yercak3_id", ["l4yercak3Id"])
|
|
208
|
+
.index("by_email", ["email"]),
|
|
209
|
+
|
|
210
|
+
// Local cache of events
|
|
211
|
+
events: defineTable({
|
|
212
|
+
l4yercak3Id: v.string(),
|
|
213
|
+
name: v.string(),
|
|
214
|
+
description: v.optional(v.string()),
|
|
215
|
+
startDate: v.number(),
|
|
216
|
+
endDate: v.number(),
|
|
217
|
+
location: v.string(),
|
|
218
|
+
status: v.string(),
|
|
219
|
+
maxCapacity: v.optional(v.number()),
|
|
220
|
+
syncedAt: v.number(),
|
|
221
|
+
})
|
|
222
|
+
.index("by_l4yercak3_id", ["l4yercak3Id"])
|
|
223
|
+
.index("by_status", ["status"]),
|
|
224
|
+
|
|
225
|
+
// Local orders/purchases
|
|
226
|
+
orders: defineTable({
|
|
227
|
+
l4yercak3Id: v.optional(v.string()),
|
|
228
|
+
contactId: v.id("contacts"),
|
|
229
|
+
eventId: v.optional(v.id("events")),
|
|
230
|
+
items: v.array(v.object({
|
|
231
|
+
productId: v.string(),
|
|
232
|
+
name: v.string(),
|
|
233
|
+
quantity: v.number(),
|
|
234
|
+
priceInCents: v.number(),
|
|
235
|
+
})),
|
|
236
|
+
totalInCents: v.number(),
|
|
237
|
+
currency: v.string(),
|
|
238
|
+
status: v.string(),
|
|
239
|
+
stripePaymentIntentId: v.optional(v.string()),
|
|
240
|
+
createdAt: v.number(),
|
|
241
|
+
syncedAt: v.optional(v.number()),
|
|
242
|
+
})
|
|
243
|
+
.index("by_contact", ["contactId"])
|
|
244
|
+
.index("by_status", ["status"]),
|
|
245
|
+
|
|
246
|
+
// Form submissions (local + synced)
|
|
247
|
+
formSubmissions: defineTable({
|
|
248
|
+
l4yercak3FormId: v.string(),
|
|
249
|
+
l4yercak3ResponseId: v.optional(v.string()),
|
|
250
|
+
contactId: v.optional(v.id("contacts")),
|
|
251
|
+
data: v.any(),
|
|
252
|
+
submittedAt: v.number(),
|
|
253
|
+
syncedAt: v.optional(v.number()),
|
|
254
|
+
})
|
|
255
|
+
.index("by_form", ["l4yercak3FormId"]),
|
|
256
|
+
|
|
257
|
+
// Sync metadata
|
|
258
|
+
syncStatus: defineTable({
|
|
259
|
+
entityType: v.string(), // "contacts", "events", etc.
|
|
260
|
+
lastSyncAt: v.number(),
|
|
261
|
+
lastSyncCursor: v.optional(v.string()),
|
|
262
|
+
status: v.string(), // "idle", "syncing", "error"
|
|
263
|
+
errorMessage: v.optional(v.string()),
|
|
264
|
+
})
|
|
265
|
+
.index("by_entity", ["entityType"]),
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Sync Strategy
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// convex/sync.ts
|
|
273
|
+
import { internalMutation, internalQuery } from "./_generated/server";
|
|
274
|
+
import { v } from "convex/values";
|
|
275
|
+
|
|
276
|
+
// Bidirectional sync with L4YERCAK3
|
|
277
|
+
export const syncContacts = internalMutation({
|
|
278
|
+
args: { direction: v.union(v.literal("push"), v.literal("pull"), v.literal("both")) },
|
|
279
|
+
handler: async (ctx, { direction }) => {
|
|
280
|
+
// 1. Pull changes from L4YERCAK3
|
|
281
|
+
if (direction === "pull" || direction === "both") {
|
|
282
|
+
const lastSync = await ctx.db
|
|
283
|
+
.query("syncStatus")
|
|
284
|
+
.withIndex("by_entity", (q) => q.eq("entityType", "contacts"))
|
|
285
|
+
.first();
|
|
286
|
+
|
|
287
|
+
// Fetch from L4YERCAK3 API
|
|
288
|
+
// Update local records
|
|
289
|
+
// Track sync cursor
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 2. Push local changes to L4YERCAK3
|
|
293
|
+
if (direction === "push" || direction === "both") {
|
|
294
|
+
const localOnlyContacts = await ctx.db
|
|
295
|
+
.query("contacts")
|
|
296
|
+
.filter((q) => q.eq(q.field("localOnly"), true))
|
|
297
|
+
.collect();
|
|
298
|
+
|
|
299
|
+
// Push to L4YERCAK3 API
|
|
300
|
+
// Update l4yercak3Id on local records
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Integration Path 2: API Only
|
|
309
|
+
|
|
310
|
+
**Target User:** Developers who want full control over UI but need a solid API foundation.
|
|
311
|
+
|
|
312
|
+
### What Gets Generated
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
project/
|
|
316
|
+
├── .env.local # API keys
|
|
317
|
+
├── l4yercak3.config.ts # Configuration
|
|
318
|
+
│
|
|
319
|
+
└── lib/
|
|
320
|
+
└── l4yercak3/
|
|
321
|
+
├── client.ts # Full typed API client
|
|
322
|
+
├── types.ts # All TypeScript types
|
|
323
|
+
├── auth.ts # Authentication helpers
|
|
324
|
+
├── webhooks.ts # Webhook handler utilities
|
|
325
|
+
└── index.ts # Main export
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Generated API Client
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// lib/l4yercak3/client.ts
|
|
332
|
+
import type {
|
|
333
|
+
Contact, ContactCreateInput, ContactUpdateInput,
|
|
334
|
+
Event, EventCreateInput,
|
|
335
|
+
Form, FormSubmission,
|
|
336
|
+
Product, Order,
|
|
337
|
+
// ... all types
|
|
338
|
+
} from './types';
|
|
339
|
+
|
|
340
|
+
export class L4yercak3Client {
|
|
341
|
+
private apiKey: string;
|
|
342
|
+
private baseUrl: string;
|
|
343
|
+
|
|
344
|
+
constructor(config: { apiKey: string; baseUrl?: string }) {
|
|
345
|
+
this.apiKey = config.apiKey;
|
|
346
|
+
this.baseUrl = config.baseUrl || 'https://api.l4yercak3.com';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============ CRM ============
|
|
350
|
+
|
|
351
|
+
async listContacts(params?: {
|
|
352
|
+
limit?: number;
|
|
353
|
+
status?: 'active' | 'inactive' | 'archived';
|
|
354
|
+
search?: string;
|
|
355
|
+
}): Promise<{ contacts: Contact[]; total: number }> {
|
|
356
|
+
return this.request('GET', '/api/v1/crm/contacts', { params });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async getContact(id: string, options?: {
|
|
360
|
+
includeActivities?: boolean;
|
|
361
|
+
includeNotes?: boolean;
|
|
362
|
+
}): Promise<Contact> {
|
|
363
|
+
return this.request('GET', `/api/v1/crm/contacts/${id}`, { params: options });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async createContact(data: ContactCreateInput): Promise<Contact> {
|
|
367
|
+
return this.request('POST', '/api/v1/crm/contacts', { body: data });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async updateContact(id: string, data: ContactUpdateInput): Promise<Contact> {
|
|
371
|
+
return this.request('PATCH', `/api/v1/crm/contacts/${id}`, { body: data });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async deleteContact(id: string): Promise<void> {
|
|
375
|
+
return this.request('DELETE', `/api/v1/crm/contacts/${id}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============ Events ============
|
|
379
|
+
|
|
380
|
+
async listEvents(params?: {
|
|
381
|
+
status?: 'draft' | 'published' | 'cancelled';
|
|
382
|
+
fromDate?: string;
|
|
383
|
+
toDate?: string;
|
|
384
|
+
limit?: number;
|
|
385
|
+
}): Promise<{ events: Event[]; total: number }> {
|
|
386
|
+
return this.request('GET', '/api/v1/events', { params });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async getEvent(id: string, options?: {
|
|
390
|
+
includeProducts?: boolean;
|
|
391
|
+
includeSponsors?: boolean;
|
|
392
|
+
}): Promise<Event> {
|
|
393
|
+
return this.request('GET', `/api/v1/events/${id}`, { params: options });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async createEvent(data: EventCreateInput): Promise<Event> {
|
|
397
|
+
return this.request('POST', '/api/v1/events', { body: data });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async getEventAttendees(eventId: string, params?: {
|
|
401
|
+
status?: 'registered' | 'checked_in' | 'cancelled';
|
|
402
|
+
limit?: number;
|
|
403
|
+
}): Promise<{ attendees: Attendee[]; total: number }> {
|
|
404
|
+
return this.request('GET', `/api/v1/events/${eventId}/attendees`, { params });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============ Forms ============
|
|
408
|
+
|
|
409
|
+
async listForms(params?: {
|
|
410
|
+
status?: 'draft' | 'published';
|
|
411
|
+
eventId?: string;
|
|
412
|
+
}): Promise<{ forms: Form[]; total: number }> {
|
|
413
|
+
return this.request('GET', '/api/v1/forms', { params });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async getForm(id: string): Promise<Form> {
|
|
417
|
+
return this.request('GET', `/api/v1/forms/${id}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async submitForm(formId: string, data: Record<string, unknown>): Promise<FormSubmission> {
|
|
421
|
+
return this.request('POST', `/api/v1/forms/${formId}/submit`, { body: data });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async getFormResponses(formId: string, params?: {
|
|
425
|
+
limit?: number;
|
|
426
|
+
offset?: number;
|
|
427
|
+
}): Promise<{ responses: FormSubmission[]; total: number }> {
|
|
428
|
+
return this.request('GET', `/api/v1/forms/${formId}/responses`, { params });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============ Products & Checkout ============
|
|
432
|
+
|
|
433
|
+
async listProducts(params?: {
|
|
434
|
+
eventId?: string;
|
|
435
|
+
status?: 'active' | 'sold_out' | 'hidden';
|
|
436
|
+
}): Promise<{ products: Product[]; total: number }> {
|
|
437
|
+
return this.request('GET', '/api/v1/products', { params });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async createCheckoutSession(data: {
|
|
441
|
+
items: Array<{ productId: string; quantity: number }>;
|
|
442
|
+
contactId?: string;
|
|
443
|
+
successUrl: string;
|
|
444
|
+
cancelUrl: string;
|
|
445
|
+
}): Promise<{ sessionId: string; checkoutUrl: string }> {
|
|
446
|
+
return this.request('POST', '/api/v1/checkout/sessions', { body: data });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============ Invoicing ============
|
|
450
|
+
|
|
451
|
+
async listInvoices(params?: {
|
|
452
|
+
contactId?: string;
|
|
453
|
+
status?: 'draft' | 'sent' | 'paid' | 'overdue';
|
|
454
|
+
}): Promise<{ invoices: Invoice[]; total: number }> {
|
|
455
|
+
return this.request('GET', '/api/v1/invoices', { params });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async createInvoice(data: InvoiceCreateInput): Promise<Invoice> {
|
|
459
|
+
return this.request('POST', '/api/v1/invoices', { body: data });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async sendInvoice(id: string): Promise<void> {
|
|
463
|
+
return this.request('POST', `/api/v1/invoices/${id}/send`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ============ Benefits ============
|
|
467
|
+
|
|
468
|
+
async listBenefitClaims(params?: {
|
|
469
|
+
status?: 'pending' | 'approved' | 'rejected' | 'paid';
|
|
470
|
+
memberId?: string;
|
|
471
|
+
}): Promise<{ claims: BenefitClaim[]; total: number }> {
|
|
472
|
+
return this.request('GET', '/api/v1/benefits/claims', { params });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async createBenefitClaim(data: BenefitClaimInput): Promise<BenefitClaim> {
|
|
476
|
+
return this.request('POST', '/api/v1/benefits/claims', { body: data });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ============ Internal ============
|
|
480
|
+
|
|
481
|
+
private async request<T>(
|
|
482
|
+
method: string,
|
|
483
|
+
path: string,
|
|
484
|
+
options?: { params?: Record<string, unknown>; body?: unknown }
|
|
485
|
+
): Promise<T> {
|
|
486
|
+
const url = new URL(path, this.baseUrl);
|
|
487
|
+
|
|
488
|
+
if (options?.params) {
|
|
489
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
|
490
|
+
if (value !== undefined) {
|
|
491
|
+
url.searchParams.set(key, String(value));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const response = await fetch(url.toString(), {
|
|
497
|
+
method,
|
|
498
|
+
headers: {
|
|
499
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
500
|
+
'Content-Type': 'application/json',
|
|
501
|
+
},
|
|
502
|
+
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
const error = await response.json().catch(() => ({}));
|
|
507
|
+
throw new L4yercak3Error(response.status, error.message || 'Request failed', error);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return response.json();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export class L4yercak3Error extends Error {
|
|
515
|
+
constructor(
|
|
516
|
+
public status: number,
|
|
517
|
+
message: string,
|
|
518
|
+
public details?: unknown
|
|
519
|
+
) {
|
|
520
|
+
super(message);
|
|
521
|
+
this.name = 'L4yercak3Error';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Singleton instance
|
|
526
|
+
let client: L4yercak3Client | null = null;
|
|
527
|
+
|
|
528
|
+
export function getL4yercak3Client(): L4yercak3Client {
|
|
529
|
+
if (!client) {
|
|
530
|
+
const apiKey = process.env.L4YERCAK3_API_KEY;
|
|
531
|
+
if (!apiKey) {
|
|
532
|
+
throw new Error('L4YERCAK3_API_KEY environment variable is required');
|
|
533
|
+
}
|
|
534
|
+
client = new L4yercak3Client({ apiKey });
|
|
535
|
+
}
|
|
536
|
+
return client;
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Generated Types
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// lib/l4yercak3/types.ts
|
|
544
|
+
|
|
545
|
+
// ============ CRM Types ============
|
|
546
|
+
|
|
547
|
+
export interface Contact {
|
|
548
|
+
id: string;
|
|
549
|
+
firstName: string;
|
|
550
|
+
lastName: string;
|
|
551
|
+
email: string;
|
|
552
|
+
phone?: string;
|
|
553
|
+
company?: string;
|
|
554
|
+
jobTitle?: string;
|
|
555
|
+
status: 'active' | 'inactive' | 'unsubscribed' | 'archived';
|
|
556
|
+
subtype: 'customer' | 'lead' | 'prospect' | 'partner';
|
|
557
|
+
tags: string[];
|
|
558
|
+
customFields?: Record<string, unknown>;
|
|
559
|
+
createdAt: string;
|
|
560
|
+
updatedAt: string;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export interface ContactCreateInput {
|
|
564
|
+
firstName: string;
|
|
565
|
+
lastName: string;
|
|
566
|
+
email: string;
|
|
567
|
+
phone?: string;
|
|
568
|
+
company?: string;
|
|
569
|
+
jobTitle?: string;
|
|
570
|
+
subtype?: 'customer' | 'lead' | 'prospect' | 'partner';
|
|
571
|
+
tags?: string[];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export interface ContactUpdateInput {
|
|
575
|
+
firstName?: string;
|
|
576
|
+
lastName?: string;
|
|
577
|
+
email?: string;
|
|
578
|
+
phone?: string;
|
|
579
|
+
company?: string;
|
|
580
|
+
jobTitle?: string;
|
|
581
|
+
status?: 'active' | 'inactive' | 'unsubscribed';
|
|
582
|
+
tags?: string[];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface Organization {
|
|
586
|
+
id: string;
|
|
587
|
+
name: string;
|
|
588
|
+
website?: string;
|
|
589
|
+
industry?: string;
|
|
590
|
+
size?: 'small' | 'medium' | 'large' | 'enterprise';
|
|
591
|
+
subtype: 'customer' | 'prospect' | 'partner' | 'vendor';
|
|
592
|
+
address?: Address;
|
|
593
|
+
taxId?: string;
|
|
594
|
+
contacts?: Contact[];
|
|
595
|
+
createdAt: string;
|
|
596
|
+
updatedAt: string;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ============ Event Types ============
|
|
600
|
+
|
|
601
|
+
export interface Event {
|
|
602
|
+
id: string;
|
|
603
|
+
name: string;
|
|
604
|
+
description?: string;
|
|
605
|
+
startDate: string;
|
|
606
|
+
endDate: string;
|
|
607
|
+
timezone: string;
|
|
608
|
+
location: string;
|
|
609
|
+
venue?: Venue;
|
|
610
|
+
status: 'draft' | 'published' | 'cancelled' | 'completed';
|
|
611
|
+
subtype: 'conference' | 'workshop' | 'webinar' | 'meetup' | 'other';
|
|
612
|
+
maxCapacity?: number;
|
|
613
|
+
imageUrl?: string;
|
|
614
|
+
products?: Product[];
|
|
615
|
+
sponsors?: Sponsor[];
|
|
616
|
+
createdAt: string;
|
|
617
|
+
updatedAt: string;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface EventCreateInput {
|
|
621
|
+
name: string;
|
|
622
|
+
description?: string;
|
|
623
|
+
startDate: string;
|
|
624
|
+
endDate: string;
|
|
625
|
+
location: string;
|
|
626
|
+
subtype?: 'conference' | 'workshop' | 'webinar' | 'meetup' | 'other';
|
|
627
|
+
maxCapacity?: number;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export interface Attendee {
|
|
631
|
+
id: string;
|
|
632
|
+
contactId: string;
|
|
633
|
+
contact: Contact;
|
|
634
|
+
eventId: string;
|
|
635
|
+
ticketId: string;
|
|
636
|
+
ticketName: string;
|
|
637
|
+
status: 'registered' | 'checked_in' | 'cancelled' | 'no_show';
|
|
638
|
+
checkedInAt?: string;
|
|
639
|
+
registeredAt: string;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ============ Form Types ============
|
|
643
|
+
|
|
644
|
+
export interface Form {
|
|
645
|
+
id: string;
|
|
646
|
+
name: string;
|
|
647
|
+
description?: string;
|
|
648
|
+
status: 'draft' | 'published' | 'closed';
|
|
649
|
+
subtype: 'registration' | 'survey' | 'application' | 'feedback' | 'contact';
|
|
650
|
+
eventId?: string;
|
|
651
|
+
fields: FormField[];
|
|
652
|
+
settings: FormSettings;
|
|
653
|
+
responseCount: number;
|
|
654
|
+
createdAt: string;
|
|
655
|
+
updatedAt: string;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export interface FormField {
|
|
659
|
+
id: string;
|
|
660
|
+
type: 'text' | 'email' | 'phone' | 'number' | 'select' | 'multiselect' | 'checkbox' | 'date' | 'file' | 'textarea';
|
|
661
|
+
label: string;
|
|
662
|
+
required: boolean;
|
|
663
|
+
placeholder?: string;
|
|
664
|
+
options?: string[];
|
|
665
|
+
validation?: {
|
|
666
|
+
min?: number;
|
|
667
|
+
max?: number;
|
|
668
|
+
pattern?: string;
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export interface FormSettings {
|
|
673
|
+
submitButtonText: string;
|
|
674
|
+
confirmationMessage: string;
|
|
675
|
+
redirectUrl?: string;
|
|
676
|
+
notifyEmails: string[];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export interface FormSubmission {
|
|
680
|
+
id: string;
|
|
681
|
+
formId: string;
|
|
682
|
+
contactId?: string;
|
|
683
|
+
data: Record<string, unknown>;
|
|
684
|
+
status: 'submitted' | 'reviewed' | 'approved' | 'rejected';
|
|
685
|
+
submittedAt: string;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ============ Product & Checkout Types ============
|
|
689
|
+
|
|
690
|
+
export interface Product {
|
|
691
|
+
id: string;
|
|
692
|
+
name: string;
|
|
693
|
+
description?: string;
|
|
694
|
+
priceInCents: number;
|
|
695
|
+
currency: string;
|
|
696
|
+
eventId?: string;
|
|
697
|
+
category?: string;
|
|
698
|
+
status: 'active' | 'sold_out' | 'hidden';
|
|
699
|
+
inventory?: number;
|
|
700
|
+
maxPerOrder?: number;
|
|
701
|
+
salesStart?: string;
|
|
702
|
+
salesEnd?: string;
|
|
703
|
+
createdAt: string;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export interface Order {
|
|
707
|
+
id: string;
|
|
708
|
+
contactId: string;
|
|
709
|
+
contact?: Contact;
|
|
710
|
+
items: OrderItem[];
|
|
711
|
+
totalInCents: number;
|
|
712
|
+
currency: string;
|
|
713
|
+
status: 'pending' | 'paid' | 'refunded' | 'cancelled';
|
|
714
|
+
stripePaymentIntentId?: string;
|
|
715
|
+
createdAt: string;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export interface OrderItem {
|
|
719
|
+
productId: string;
|
|
720
|
+
productName: string;
|
|
721
|
+
quantity: number;
|
|
722
|
+
priceInCents: number;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ============ Invoice Types ============
|
|
726
|
+
|
|
727
|
+
export interface Invoice {
|
|
728
|
+
id: string;
|
|
729
|
+
number: string;
|
|
730
|
+
contactId: string;
|
|
731
|
+
contact?: Contact;
|
|
732
|
+
type: 'b2b' | 'b2c';
|
|
733
|
+
status: 'draft' | 'sent' | 'paid' | 'overdue' | 'cancelled';
|
|
734
|
+
issueDate: string;
|
|
735
|
+
dueDate: string;
|
|
736
|
+
paidAt?: string;
|
|
737
|
+
lineItems: InvoiceLineItem[];
|
|
738
|
+
subtotal: number;
|
|
739
|
+
tax: number;
|
|
740
|
+
taxRate: number;
|
|
741
|
+
total: number;
|
|
742
|
+
currency: string;
|
|
743
|
+
notes?: string;
|
|
744
|
+
pdfUrl?: string;
|
|
745
|
+
createdAt: string;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface InvoiceLineItem {
|
|
749
|
+
description: string;
|
|
750
|
+
quantity: number;
|
|
751
|
+
unitPrice: number;
|
|
752
|
+
amount: number;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export interface InvoiceCreateInput {
|
|
756
|
+
contactId: string;
|
|
757
|
+
type: 'b2b' | 'b2c';
|
|
758
|
+
dueDate: string;
|
|
759
|
+
lineItems: Omit<InvoiceLineItem, 'amount'>[];
|
|
760
|
+
taxRate?: number;
|
|
761
|
+
notes?: string;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ============ Benefits Types ============
|
|
765
|
+
|
|
766
|
+
export interface BenefitClaim {
|
|
767
|
+
id: string;
|
|
768
|
+
memberId: string;
|
|
769
|
+
memberName: string;
|
|
770
|
+
benefitType: string;
|
|
771
|
+
amount: number;
|
|
772
|
+
currency: string;
|
|
773
|
+
status: 'pending' | 'approved' | 'rejected' | 'paid' | 'cancelled';
|
|
774
|
+
description?: string;
|
|
775
|
+
supportingDocuments?: string[];
|
|
776
|
+
submittedAt: string;
|
|
777
|
+
processedAt?: string;
|
|
778
|
+
notes?: string;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export interface BenefitClaimInput {
|
|
782
|
+
memberId: string;
|
|
783
|
+
benefitType: string;
|
|
784
|
+
amount: number;
|
|
785
|
+
currency?: string;
|
|
786
|
+
description?: string;
|
|
787
|
+
supportingDocuments?: string[];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export interface CommissionPayout {
|
|
791
|
+
id: string;
|
|
792
|
+
memberId: string;
|
|
793
|
+
memberName: string;
|
|
794
|
+
commissionType: string;
|
|
795
|
+
amount: number;
|
|
796
|
+
currency: string;
|
|
797
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
798
|
+
sourceTransaction?: string;
|
|
799
|
+
paidAt?: string;
|
|
800
|
+
paymentMethod?: string;
|
|
801
|
+
paymentReference?: string;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ============ Common Types ============
|
|
805
|
+
|
|
806
|
+
export interface Address {
|
|
807
|
+
street?: string;
|
|
808
|
+
city?: string;
|
|
809
|
+
state?: string;
|
|
810
|
+
postalCode?: string;
|
|
811
|
+
country?: string;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export interface Venue {
|
|
815
|
+
name: string;
|
|
816
|
+
address: Address;
|
|
817
|
+
capacity?: number;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export interface Sponsor {
|
|
821
|
+
id: string;
|
|
822
|
+
organizationId: string;
|
|
823
|
+
organizationName: string;
|
|
824
|
+
level: 'platinum' | 'gold' | 'silver' | 'bronze' | 'community';
|
|
825
|
+
logoUrl?: string;
|
|
826
|
+
websiteUrl?: string;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============ Pagination ============
|
|
830
|
+
|
|
831
|
+
export interface PaginatedResponse<T> {
|
|
832
|
+
items: T[];
|
|
833
|
+
total: number;
|
|
834
|
+
hasMore: boolean;
|
|
835
|
+
nextCursor?: string;
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Integration Path 3: MCP-Assisted
|
|
842
|
+
|
|
843
|
+
**Target User:** Developers using Claude Code who want AI to generate custom integrations.
|
|
844
|
+
|
|
845
|
+
### What Gets Generated
|
|
846
|
+
|
|
847
|
+
```
|
|
848
|
+
project/
|
|
849
|
+
├── .env.local # API keys
|
|
850
|
+
├── l4yercak3.config.ts # Configuration
|
|
851
|
+
│
|
|
852
|
+
├── .claude/ # Claude Code config
|
|
853
|
+
│ └── mcp.json # MCP server configuration
|
|
854
|
+
│
|
|
855
|
+
└── docs/
|
|
856
|
+
└── L4YERCAK3_MCP_GUIDE.md # Instructions for Claude
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### MCP Configuration
|
|
860
|
+
|
|
861
|
+
```json
|
|
862
|
+
// .claude/mcp.json (or added to existing)
|
|
863
|
+
{
|
|
864
|
+
"mcpServers": {
|
|
865
|
+
"l4yercak3": {
|
|
866
|
+
"command": "npx",
|
|
867
|
+
"args": ["@l4yercak3/cli", "mcp", "start"],
|
|
868
|
+
"env": {
|
|
869
|
+
"L4YERCAK3_CONFIG_PATH": "${workspaceFolder}/.l4yercak3"
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### Generated Guide
|
|
877
|
+
|
|
878
|
+
```markdown
|
|
879
|
+
# L4YERCAK3 MCP Integration Guide
|
|
880
|
+
|
|
881
|
+
Your project is connected to L4YERCAK3! You can now use Claude Code to build
|
|
882
|
+
custom integrations using natural language.
|
|
883
|
+
|
|
884
|
+
## Available MCP Tools
|
|
885
|
+
|
|
886
|
+
### CRM (contacts:read, contacts:write)
|
|
887
|
+
- `l4yercak3_crm_list_contacts` - List and search contacts
|
|
888
|
+
- `l4yercak3_crm_create_contact` - Create new contacts
|
|
889
|
+
- `l4yercak3_crm_get_contact` - Get contact details
|
|
890
|
+
- `l4yercak3_crm_update_contact` - Update contacts
|
|
891
|
+
- `l4yercak3_crm_delete_contact` - Delete contacts
|
|
892
|
+
- ... and more
|
|
893
|
+
|
|
894
|
+
### Events (events:read, events:write)
|
|
895
|
+
- `l4yercak3_events_list` - List events
|
|
896
|
+
- `l4yercak3_events_create` - Create events
|
|
897
|
+
- `l4yercak3_events_get` - Get event details with products/sponsors
|
|
898
|
+
- `l4yercak3_events_get_attendees` - List attendees
|
|
899
|
+
- ... and more
|
|
900
|
+
|
|
901
|
+
### Forms (forms:read, forms:write)
|
|
902
|
+
- `l4yercak3_forms_list` - List forms
|
|
903
|
+
- `l4yercak3_forms_create` - Create forms with fields
|
|
904
|
+
- `l4yercak3_forms_get_responses` - Get form submissions
|
|
905
|
+
- ... and more
|
|
906
|
+
|
|
907
|
+
### Code Generation
|
|
908
|
+
- `l4yercak3_generate_api_client` - Generate typed API client
|
|
909
|
+
- `l4yercak3_generate_component` - Generate React components
|
|
910
|
+
- `l4yercak3_generate_hook` - Generate React hooks
|
|
911
|
+
- `l4yercak3_generate_page` - Generate Next.js pages
|
|
912
|
+
|
|
913
|
+
## Example Prompts
|
|
914
|
+
|
|
915
|
+
1. "Create a contact management page with search, filtering by tags,
|
|
916
|
+
and the ability to add notes"
|
|
917
|
+
|
|
918
|
+
2. "Build an event registration flow with ticket selection and
|
|
919
|
+
Stripe checkout"
|
|
920
|
+
|
|
921
|
+
3. "Generate a form builder that lets admins create custom forms
|
|
922
|
+
and view submissions"
|
|
923
|
+
|
|
924
|
+
4. "Create a dashboard showing CRM contacts, recent events, and
|
|
925
|
+
pending invoices"
|
|
926
|
+
|
|
927
|
+
5. "Build a mobile-friendly check-in scanner for event attendees"
|
|
928
|
+
|
|
929
|
+
## Your Configuration
|
|
930
|
+
|
|
931
|
+
- **Organization:** ${organizationName}
|
|
932
|
+
- **Features Enabled:** ${features.join(', ')}
|
|
933
|
+
- **API Key:** Stored in .env.local
|
|
934
|
+
|
|
935
|
+
## Tips
|
|
936
|
+
|
|
937
|
+
- Claude can read your existing code and generate components that match your style
|
|
938
|
+
- Ask Claude to explain what MCP tools are available before starting
|
|
939
|
+
- Use Claude to set up webhooks for real-time updates from L4YERCAK3
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
## Database Detection & Setup
|
|
945
|
+
|
|
946
|
+
### Detection Logic
|
|
947
|
+
|
|
948
|
+
```javascript
|
|
949
|
+
// src/detectors/database-detector.js
|
|
950
|
+
|
|
951
|
+
async function detectDatabase(projectPath) {
|
|
952
|
+
const detections = [];
|
|
953
|
+
|
|
954
|
+
// Check for Convex
|
|
955
|
+
if (await fileExists(path.join(projectPath, 'convex'))) {
|
|
956
|
+
detections.push({
|
|
957
|
+
type: 'convex',
|
|
958
|
+
confidence: 'high',
|
|
959
|
+
configPath: 'convex/',
|
|
960
|
+
hasSchema: await fileExists(path.join(projectPath, 'convex/schema.ts')),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Check for Supabase
|
|
965
|
+
if (await fileExists(path.join(projectPath, 'supabase'))) {
|
|
966
|
+
detections.push({
|
|
967
|
+
type: 'supabase',
|
|
968
|
+
confidence: 'high',
|
|
969
|
+
configPath: 'supabase/',
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Check package.json for DB clients
|
|
974
|
+
const packageJson = await readPackageJson(projectPath);
|
|
975
|
+
if (packageJson) {
|
|
976
|
+
if (packageJson.dependencies?.['convex']) {
|
|
977
|
+
detections.push({ type: 'convex', confidence: 'medium', source: 'package.json' });
|
|
978
|
+
}
|
|
979
|
+
if (packageJson.dependencies?.['@supabase/supabase-js']) {
|
|
980
|
+
detections.push({ type: 'supabase', confidence: 'medium', source: 'package.json' });
|
|
981
|
+
}
|
|
982
|
+
if (packageJson.dependencies?.['prisma'] || packageJson.dependencies?.['@prisma/client']) {
|
|
983
|
+
detections.push({ type: 'prisma', confidence: 'medium', source: 'package.json' });
|
|
984
|
+
}
|
|
985
|
+
if (packageJson.dependencies?.['drizzle-orm']) {
|
|
986
|
+
detections.push({ type: 'drizzle', confidence: 'medium', source: 'package.json' });
|
|
987
|
+
}
|
|
988
|
+
if (packageJson.dependencies?.['mongoose']) {
|
|
989
|
+
detections.push({ type: 'mongodb', confidence: 'medium', source: 'package.json' });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Check for Prisma schema
|
|
994
|
+
if (await fileExists(path.join(projectPath, 'prisma/schema.prisma'))) {
|
|
995
|
+
detections.push({ type: 'prisma', confidence: 'high', configPath: 'prisma/' });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Check for Drizzle
|
|
999
|
+
if (await fileExists(path.join(projectPath, 'drizzle.config.ts'))) {
|
|
1000
|
+
detections.push({ type: 'drizzle', confidence: 'high' });
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
hasDatabase: detections.length > 0,
|
|
1005
|
+
detections,
|
|
1006
|
+
primary: detections.sort((a, b) =>
|
|
1007
|
+
(b.confidence === 'high' ? 1 : 0) - (a.confidence === 'high' ? 1 : 0)
|
|
1008
|
+
)[0] || null,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### Database Selection Prompt
|
|
1014
|
+
|
|
1015
|
+
```javascript
|
|
1016
|
+
// In spread.js, after feature selection
|
|
1017
|
+
|
|
1018
|
+
const dbDetection = await detectDatabase(projectPath);
|
|
1019
|
+
|
|
1020
|
+
if (!dbDetection.hasDatabase && integrationPath === 'quickstart') {
|
|
1021
|
+
console.log(chalk.yellow('\n No database detected in your project.\n'));
|
|
1022
|
+
|
|
1023
|
+
const { database } = await inquirer.prompt([
|
|
1024
|
+
{
|
|
1025
|
+
type: 'list',
|
|
1026
|
+
name: 'database',
|
|
1027
|
+
message: 'Which database would you like to use?',
|
|
1028
|
+
choices: [
|
|
1029
|
+
{
|
|
1030
|
+
name: 'Convex (Recommended) - Real-time, serverless, TypeScript-first',
|
|
1031
|
+
value: 'convex',
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: 'Supabase - PostgreSQL with Auth, Storage, and Edge Functions',
|
|
1035
|
+
value: 'supabase',
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: 'None - I\'ll set up my own database later',
|
|
1039
|
+
value: 'none',
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
},
|
|
1043
|
+
]);
|
|
1044
|
+
|
|
1045
|
+
if (database !== 'none') {
|
|
1046
|
+
await setupDatabase(projectPath, database, selectedFeatures);
|
|
1047
|
+
}
|
|
1048
|
+
} else if (dbDetection.hasDatabase) {
|
|
1049
|
+
console.log(chalk.green(`\n ✓ Detected ${dbDetection.primary.type} database\n`));
|
|
1050
|
+
}
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
---
|
|
1054
|
+
|
|
1055
|
+
## File Structure Summary
|
|
1056
|
+
|
|
1057
|
+
```
|
|
1058
|
+
src/
|
|
1059
|
+
├── commands/
|
|
1060
|
+
│ └── spread.js # Updated with 3-path flow
|
|
1061
|
+
│
|
|
1062
|
+
├── detectors/
|
|
1063
|
+
│ ├── index.js
|
|
1064
|
+
│ ├── nextjs-detector.js
|
|
1065
|
+
│ ├── database-detector.js # NEW
|
|
1066
|
+
│ └── ...
|
|
1067
|
+
│
|
|
1068
|
+
├── generators/
|
|
1069
|
+
│ ├── quickstart/ # NEW - Full stack generation
|
|
1070
|
+
│ │ ├── index.js
|
|
1071
|
+
│ │ ├── components/
|
|
1072
|
+
│ │ │ ├── crm.js
|
|
1073
|
+
│ │ │ ├── events.js
|
|
1074
|
+
│ │ │ ├── forms.js
|
|
1075
|
+
│ │ │ ├── checkout.js
|
|
1076
|
+
│ │ │ └── ...
|
|
1077
|
+
│ │ ├── hooks/
|
|
1078
|
+
│ │ │ └── index.js
|
|
1079
|
+
│ │ ├── pages/
|
|
1080
|
+
│ │ │ └── index.js
|
|
1081
|
+
│ │ └── database/
|
|
1082
|
+
│ │ ├── convex.js
|
|
1083
|
+
│ │ └── supabase.js
|
|
1084
|
+
│ │
|
|
1085
|
+
│ ├── api-only/ # NEW - API client generation
|
|
1086
|
+
│ │ ├── index.js
|
|
1087
|
+
│ │ ├── client.js
|
|
1088
|
+
│ │ ├── types.js
|
|
1089
|
+
│ │ └── webhooks.js
|
|
1090
|
+
│ │
|
|
1091
|
+
│ ├── mcp-assisted/ # NEW - MCP setup
|
|
1092
|
+
│ │ ├── index.js
|
|
1093
|
+
│ │ ├── config.js
|
|
1094
|
+
│ │ └── guide.js
|
|
1095
|
+
│ │
|
|
1096
|
+
│ └── ... (existing generators)
|
|
1097
|
+
│
|
|
1098
|
+
└── templates/ # NEW - Template files
|
|
1099
|
+
├── components/
|
|
1100
|
+
│ ├── ContactList.tsx.template
|
|
1101
|
+
│ ├── EventCard.tsx.template
|
|
1102
|
+
│ └── ...
|
|
1103
|
+
├── hooks/
|
|
1104
|
+
│ ├── useContacts.ts.template
|
|
1105
|
+
│ └── ...
|
|
1106
|
+
├── pages/
|
|
1107
|
+
│ ├── crm.tsx.template
|
|
1108
|
+
│ └── ...
|
|
1109
|
+
└── database/
|
|
1110
|
+
├── convex-schema.ts.template
|
|
1111
|
+
└── supabase-schema.sql.template
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
---
|
|
1115
|
+
|
|
1116
|
+
## Implementation Priority
|
|
1117
|
+
|
|
1118
|
+
### Phase 1: Foundation
|
|
1119
|
+
1. Update `spread.js` with 3-path selection
|
|
1120
|
+
2. Create database detector
|
|
1121
|
+
3. Implement API-only generator (client + types)
|
|
1122
|
+
|
|
1123
|
+
### Phase 2: Quick Start
|
|
1124
|
+
4. Create component templates for each feature
|
|
1125
|
+
5. Implement Convex database setup
|
|
1126
|
+
6. Implement Supabase database setup
|
|
1127
|
+
7. Create page generators
|
|
1128
|
+
|
|
1129
|
+
### Phase 3: MCP Enhancement
|
|
1130
|
+
8. Update MCP server config generator
|
|
1131
|
+
9. Create comprehensive MCP guide generator
|
|
1132
|
+
10. Add code generation MCP tools
|
|
1133
|
+
|
|
1134
|
+
### Phase 4: Polish
|
|
1135
|
+
11. Add progress indicators
|
|
1136
|
+
12. Improve error handling
|
|
1137
|
+
13. Add rollback on failure
|
|
1138
|
+
14. Write tests
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
1142
|
+
## Core Design Principles
|
|
1143
|
+
|
|
1144
|
+
### 1. Ontology-First Database Design
|
|
1145
|
+
|
|
1146
|
+
Instead of creating separate tables for each entity (contacts, events, forms), we mirror L4YERCAK3's
|
|
1147
|
+
**ontology pattern** with a universal `objects` table. This provides:
|
|
1148
|
+
|
|
1149
|
+
- **Flexibility**: Add new entity types without schema changes
|
|
1150
|
+
- **Consistency**: Same sync logic for all entity types
|
|
1151
|
+
- **Compatibility**: Direct mapping to L4YERCAK3 backend structure
|
|
1152
|
+
|
|
1153
|
+
```typescript
|
|
1154
|
+
// The objects table stores ALL entity types
|
|
1155
|
+
objects: defineTable({
|
|
1156
|
+
l4yercak3Id: v.optional(v.string()), // null if local-only
|
|
1157
|
+
type: v.string(), // "contact", "event", "form", etc.
|
|
1158
|
+
subtype: v.optional(v.string()), // "customer", "conference", etc.
|
|
1159
|
+
name: v.string(),
|
|
1160
|
+
status: v.string(),
|
|
1161
|
+
customProperties: v.any(), // Type-specific data
|
|
1162
|
+
syncStatus: v.string(), // "synced", "pending_push", etc.
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
// Relationships between objects
|
|
1166
|
+
objectLinks: defineTable({
|
|
1167
|
+
fromObjectId: v.id("objects"),
|
|
1168
|
+
toObjectId: v.id("objects"),
|
|
1169
|
+
linkType: v.string(), // "attendee", "sponsor", etc.
|
|
1170
|
+
})
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### 2. Frontend User Management
|
|
1174
|
+
|
|
1175
|
+
Users authenticate **locally** (OAuth, credentials) but sync to L4YERCAK3 as CRM contacts:
|
|
1176
|
+
|
|
1177
|
+
```
|
|
1178
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1179
|
+
│ LOCAL (Your App) │ L4YERCAK3 BACKEND │
|
|
1180
|
+
├────────────────────────────────┼─────────────────────────┤
|
|
1181
|
+
│ NextAuth.js / Supabase Auth │ │
|
|
1182
|
+
│ ├─ OAuth tokens (encrypted) │ │
|
|
1183
|
+
│ ├─ Session management │ │
|
|
1184
|
+
│ ├─ Password hashes │ │
|
|
1185
|
+
│ └─ Provider connections │ │
|
|
1186
|
+
│ │ │
|
|
1187
|
+
│ frontendUsers table ──────────┼──► CRM Contact │
|
|
1188
|
+
│ ├─ email, name, image │ (auto-created) │
|
|
1189
|
+
│ ├─ l4yercak3ContactId ◄───────┼─── contactId │
|
|
1190
|
+
│ └─ role, metadata │ │
|
|
1191
|
+
│ │ │
|
|
1192
|
+
│ User actions ─────────────────┼──► Activity tracking │
|
|
1193
|
+
│ ├─ Event registrations │ Purchase history │
|
|
1194
|
+
│ ├─ Form submissions │ Engagement metrics │
|
|
1195
|
+
│ └─ Purchases │ │
|
|
1196
|
+
└──────────────────────────────────────────────────────────┘
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
**Why local auth?**
|
|
1200
|
+
- OAuth redirect URLs must match your domain
|
|
1201
|
+
- Sessions need low-latency local access
|
|
1202
|
+
- Tokens must be securely stored locally
|
|
1203
|
+
- L4YERCAK3 backend tracks the *business relationship*, not auth credentials
|
|
1204
|
+
|
|
1205
|
+
### 3. Stripe Integration Pattern
|
|
1206
|
+
|
|
1207
|
+
Stripe API calls happen **locally** (your keys, your webhooks), but transactions sync to L4YERCAK3:
|
|
1208
|
+
|
|
1209
|
+
```typescript
|
|
1210
|
+
// Local: Handle Stripe webhook
|
|
1211
|
+
export async function POST(req: Request) {
|
|
1212
|
+
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
|
|
1213
|
+
|
|
1214
|
+
if (event.type === 'payment_intent.succeeded') {
|
|
1215
|
+
// 1. Store locally first (immediate)
|
|
1216
|
+
await db.insert('stripePayments', {
|
|
1217
|
+
stripePaymentIntentId: event.data.object.id,
|
|
1218
|
+
amount: event.data.object.amount,
|
|
1219
|
+
status: 'succeeded',
|
|
1220
|
+
syncStatus: 'pending_push',
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// 2. Sync to L4YERCAK3 (async)
|
|
1224
|
+
await syncPaymentToL4yercak3(event.data.object);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Sync creates Order + Invoice objects in L4YERCAK3
|
|
1229
|
+
async function syncPaymentToL4yercak3(payment) {
|
|
1230
|
+
const order = await l4yercak3.createOrder({
|
|
1231
|
+
contactId: payment.metadata.l4yercak3ContactId,
|
|
1232
|
+
items: JSON.parse(payment.metadata.items),
|
|
1233
|
+
totalInCents: payment.amount,
|
|
1234
|
+
stripePaymentIntentId: payment.id,
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// Update local record with L4YERCAK3 ID
|
|
1238
|
+
await db.patch(localPaymentId, {
|
|
1239
|
+
l4yercak3OrderId: order.id,
|
|
1240
|
+
syncStatus: 'synced'
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
**What stays local:**
|
|
1246
|
+
- Stripe API keys and webhook secrets
|
|
1247
|
+
- Payment intent creation
|
|
1248
|
+
- Webhook endpoint handling
|
|
1249
|
+
- Stripe Customer Portal integration
|
|
1250
|
+
|
|
1251
|
+
**What syncs to L4YERCAK3:**
|
|
1252
|
+
- Order records (for CRM, reporting)
|
|
1253
|
+
- Invoice generation (if B2B)
|
|
1254
|
+
- Transaction history
|
|
1255
|
+
- Revenue analytics
|
|
1256
|
+
- Refund tracking
|
|
1257
|
+
|
|
1258
|
+
### 4. Organization Owner Perspective
|
|
1259
|
+
|
|
1260
|
+
As an organization owner using L4YERCAK3, your **frontend users** are your customers:
|
|
1261
|
+
|
|
1262
|
+
```
|
|
1263
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1264
|
+
│ YOU (Organization Owner) │
|
|
1265
|
+
│ └─► L4YERCAK3 Dashboard │
|
|
1266
|
+
│ ├─► CRM: See all your frontend users as contacts │
|
|
1267
|
+
│ ├─► Events: See registrations, check-ins │
|
|
1268
|
+
│ ├─► Forms: See submissions from your users │
|
|
1269
|
+
│ ├─► Invoicing: Generate invoices for purchases │
|
|
1270
|
+
│ └─► Analytics: User engagement, revenue, etc. │
|
|
1271
|
+
│ │
|
|
1272
|
+
│ YOUR APP (Built with L4YERCAK3 CLI) │
|
|
1273
|
+
│ └─► Your frontend users │
|
|
1274
|
+
│ ├─► Sign up/login (local auth) │
|
|
1275
|
+
│ ├─► Browse events, register (syncs to L4YERCAK3) │
|
|
1276
|
+
│ ├─► Fill forms (syncs to L4YERCAK3) │
|
|
1277
|
+
│ ├─► Purchase tickets (Stripe local, syncs order) │
|
|
1278
|
+
│ └─► View their profile, history (reads from local + sync) │
|
|
1279
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
---
|
|
1283
|
+
|
|
1284
|
+
## Database Schema (Convex - Full)
|
|
1285
|
+
|
|
1286
|
+
```typescript
|
|
1287
|
+
// convex/schema.ts
|
|
1288
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
1289
|
+
import { v } from "convex/values";
|
|
1290
|
+
|
|
1291
|
+
export default defineSchema({
|
|
1292
|
+
// ============================================
|
|
1293
|
+
// ONTOLOGY: Universal object storage
|
|
1294
|
+
// ============================================
|
|
1295
|
+
|
|
1296
|
+
objects: defineTable({
|
|
1297
|
+
// L4YERCAK3 sync
|
|
1298
|
+
l4yercak3Id: v.optional(v.string()),
|
|
1299
|
+
organizationId: v.string(),
|
|
1300
|
+
|
|
1301
|
+
// Core fields (all objects have these)
|
|
1302
|
+
type: v.string(), // "contact", "event", "form", "product", "order"
|
|
1303
|
+
subtype: v.optional(v.string()), // Type-specific classification
|
|
1304
|
+
name: v.string(),
|
|
1305
|
+
status: v.string(),
|
|
1306
|
+
|
|
1307
|
+
// Type-specific data
|
|
1308
|
+
customProperties: v.any(),
|
|
1309
|
+
|
|
1310
|
+
// Sync tracking
|
|
1311
|
+
syncStatus: v.union(
|
|
1312
|
+
v.literal("synced"),
|
|
1313
|
+
v.literal("pending_push"),
|
|
1314
|
+
v.literal("pending_pull"),
|
|
1315
|
+
v.literal("conflict"),
|
|
1316
|
+
v.literal("local_only")
|
|
1317
|
+
),
|
|
1318
|
+
syncedAt: v.optional(v.number()),
|
|
1319
|
+
localVersion: v.number(),
|
|
1320
|
+
remoteVersion: v.optional(v.number()),
|
|
1321
|
+
|
|
1322
|
+
// Timestamps
|
|
1323
|
+
createdAt: v.number(),
|
|
1324
|
+
updatedAt: v.number(),
|
|
1325
|
+
deletedAt: v.optional(v.number()), // Soft delete
|
|
1326
|
+
})
|
|
1327
|
+
.index("by_l4yercak3_id", ["l4yercak3Id"])
|
|
1328
|
+
.index("by_type", ["type"])
|
|
1329
|
+
.index("by_type_status", ["type", "status"])
|
|
1330
|
+
.index("by_type_subtype", ["type", "subtype"])
|
|
1331
|
+
.index("by_sync_status", ["syncStatus"])
|
|
1332
|
+
.index("by_updated", ["updatedAt"]),
|
|
1333
|
+
|
|
1334
|
+
objectLinks: defineTable({
|
|
1335
|
+
l4yercak3Id: v.optional(v.string()),
|
|
1336
|
+
fromObjectId: v.id("objects"),
|
|
1337
|
+
toObjectId: v.id("objects"),
|
|
1338
|
+
linkType: v.string(),
|
|
1339
|
+
metadata: v.optional(v.any()),
|
|
1340
|
+
syncStatus: v.string(),
|
|
1341
|
+
createdAt: v.number(),
|
|
1342
|
+
})
|
|
1343
|
+
.index("by_from", ["fromObjectId"])
|
|
1344
|
+
.index("by_to", ["toObjectId"])
|
|
1345
|
+
.index("by_from_type", ["fromObjectId", "linkType"])
|
|
1346
|
+
.index("by_to_type", ["toObjectId", "linkType"]),
|
|
1347
|
+
|
|
1348
|
+
// ============================================
|
|
1349
|
+
// AUTHENTICATION: Local user management
|
|
1350
|
+
// ============================================
|
|
1351
|
+
|
|
1352
|
+
frontendUsers: defineTable({
|
|
1353
|
+
// L4YERCAK3 sync
|
|
1354
|
+
l4yercak3ContactId: v.optional(v.string()),
|
|
1355
|
+
l4yercak3FrontendUserId: v.optional(v.string()),
|
|
1356
|
+
organizationId: v.string(),
|
|
1357
|
+
|
|
1358
|
+
// Core identity
|
|
1359
|
+
email: v.string(),
|
|
1360
|
+
emailVerified: v.boolean(),
|
|
1361
|
+
name: v.optional(v.string()),
|
|
1362
|
+
firstName: v.optional(v.string()),
|
|
1363
|
+
lastName: v.optional(v.string()),
|
|
1364
|
+
image: v.optional(v.string()),
|
|
1365
|
+
phone: v.optional(v.string()),
|
|
1366
|
+
|
|
1367
|
+
// Local auth
|
|
1368
|
+
passwordHash: v.optional(v.string()), // If using credentials
|
|
1369
|
+
|
|
1370
|
+
// OAuth (stored locally, not synced)
|
|
1371
|
+
oauthAccounts: v.array(v.object({
|
|
1372
|
+
provider: v.string(),
|
|
1373
|
+
providerAccountId: v.string(),
|
|
1374
|
+
accessToken: v.optional(v.string()),
|
|
1375
|
+
refreshToken: v.optional(v.string()),
|
|
1376
|
+
expiresAt: v.optional(v.number()),
|
|
1377
|
+
scope: v.optional(v.string()),
|
|
1378
|
+
})),
|
|
1379
|
+
|
|
1380
|
+
// App-specific
|
|
1381
|
+
role: v.string(), // "user", "admin", "moderator"
|
|
1382
|
+
preferences: v.optional(v.object({
|
|
1383
|
+
language: v.optional(v.string()),
|
|
1384
|
+
timezone: v.optional(v.string()),
|
|
1385
|
+
theme: v.optional(v.string()),
|
|
1386
|
+
emailNotifications: v.optional(v.boolean()),
|
|
1387
|
+
})),
|
|
1388
|
+
|
|
1389
|
+
// Sync
|
|
1390
|
+
syncStatus: v.string(),
|
|
1391
|
+
syncedAt: v.optional(v.number()),
|
|
1392
|
+
|
|
1393
|
+
// Timestamps
|
|
1394
|
+
createdAt: v.number(),
|
|
1395
|
+
updatedAt: v.number(),
|
|
1396
|
+
lastLoginAt: v.optional(v.number()),
|
|
1397
|
+
})
|
|
1398
|
+
.index("by_email", ["email"])
|
|
1399
|
+
.index("by_l4yercak3_contact", ["l4yercak3ContactId"])
|
|
1400
|
+
.index("by_oauth", ["oauthAccounts"]),
|
|
1401
|
+
|
|
1402
|
+
sessions: defineTable({
|
|
1403
|
+
userId: v.id("frontendUsers"),
|
|
1404
|
+
sessionToken: v.string(),
|
|
1405
|
+
expiresAt: v.number(),
|
|
1406
|
+
userAgent: v.optional(v.string()),
|
|
1407
|
+
ipAddress: v.optional(v.string()),
|
|
1408
|
+
createdAt: v.number(),
|
|
1409
|
+
})
|
|
1410
|
+
.index("by_token", ["sessionToken"])
|
|
1411
|
+
.index("by_user", ["userId"]),
|
|
1412
|
+
|
|
1413
|
+
// ============================================
|
|
1414
|
+
// STRIPE: Local payment handling
|
|
1415
|
+
// ============================================
|
|
1416
|
+
|
|
1417
|
+
stripeCustomers: defineTable({
|
|
1418
|
+
frontendUserId: v.id("frontendUsers"),
|
|
1419
|
+
stripeCustomerId: v.string(),
|
|
1420
|
+
l4yercak3ContactId: v.optional(v.string()),
|
|
1421
|
+
email: v.string(),
|
|
1422
|
+
name: v.optional(v.string()),
|
|
1423
|
+
defaultPaymentMethodId: v.optional(v.string()),
|
|
1424
|
+
syncStatus: v.string(),
|
|
1425
|
+
createdAt: v.number(),
|
|
1426
|
+
})
|
|
1427
|
+
.index("by_stripe_id", ["stripeCustomerId"])
|
|
1428
|
+
.index("by_user", ["frontendUserId"]),
|
|
1429
|
+
|
|
1430
|
+
stripePayments: defineTable({
|
|
1431
|
+
stripePaymentIntentId: v.string(),
|
|
1432
|
+
stripeCustomerId: v.optional(v.string()),
|
|
1433
|
+
frontendUserId: v.optional(v.id("frontendUsers")),
|
|
1434
|
+
|
|
1435
|
+
// Payment details
|
|
1436
|
+
amount: v.number(),
|
|
1437
|
+
currency: v.string(),
|
|
1438
|
+
status: v.string(),
|
|
1439
|
+
paymentMethod: v.optional(v.string()),
|
|
1440
|
+
|
|
1441
|
+
// What was purchased
|
|
1442
|
+
metadata: v.object({
|
|
1443
|
+
type: v.string(), // "event_ticket", "product", "subscription"
|
|
1444
|
+
items: v.array(v.object({
|
|
1445
|
+
objectId: v.optional(v.id("objects")),
|
|
1446
|
+
l4yercak3ProductId: v.optional(v.string()),
|
|
1447
|
+
name: v.string(),
|
|
1448
|
+
quantity: v.number(),
|
|
1449
|
+
priceInCents: v.number(),
|
|
1450
|
+
})),
|
|
1451
|
+
eventId: v.optional(v.string()),
|
|
1452
|
+
}),
|
|
1453
|
+
|
|
1454
|
+
// L4YERCAK3 sync
|
|
1455
|
+
l4yercak3OrderId: v.optional(v.string()),
|
|
1456
|
+
l4yercak3InvoiceId: v.optional(v.string()),
|
|
1457
|
+
syncStatus: v.string(),
|
|
1458
|
+
syncedAt: v.optional(v.number()),
|
|
1459
|
+
|
|
1460
|
+
// Timestamps
|
|
1461
|
+
createdAt: v.number(),
|
|
1462
|
+
completedAt: v.optional(v.number()),
|
|
1463
|
+
})
|
|
1464
|
+
.index("by_stripe_id", ["stripePaymentIntentId"])
|
|
1465
|
+
.index("by_customer", ["stripeCustomerId"])
|
|
1466
|
+
.index("by_user", ["frontendUserId"])
|
|
1467
|
+
.index("by_sync_status", ["syncStatus"]),
|
|
1468
|
+
|
|
1469
|
+
stripeSubscriptions: defineTable({
|
|
1470
|
+
stripeSubscriptionId: v.string(),
|
|
1471
|
+
stripeCustomerId: v.string(),
|
|
1472
|
+
frontendUserId: v.id("frontendUsers"),
|
|
1473
|
+
|
|
1474
|
+
status: v.string(),
|
|
1475
|
+
priceId: v.string(),
|
|
1476
|
+
productId: v.string(),
|
|
1477
|
+
|
|
1478
|
+
currentPeriodStart: v.number(),
|
|
1479
|
+
currentPeriodEnd: v.number(),
|
|
1480
|
+
cancelAtPeriodEnd: v.boolean(),
|
|
1481
|
+
|
|
1482
|
+
l4yercak3SubscriptionId: v.optional(v.string()),
|
|
1483
|
+
syncStatus: v.string(),
|
|
1484
|
+
|
|
1485
|
+
createdAt: v.number(),
|
|
1486
|
+
updatedAt: v.number(),
|
|
1487
|
+
})
|
|
1488
|
+
.index("by_stripe_id", ["stripeSubscriptionId"])
|
|
1489
|
+
.index("by_user", ["frontendUserId"]),
|
|
1490
|
+
|
|
1491
|
+
// ============================================
|
|
1492
|
+
// SYNC: Job tracking
|
|
1493
|
+
// ============================================
|
|
1494
|
+
|
|
1495
|
+
syncJobs: defineTable({
|
|
1496
|
+
entityType: v.string(),
|
|
1497
|
+
direction: v.union(v.literal("push"), v.literal("pull"), v.literal("bidirectional")),
|
|
1498
|
+
status: v.union(
|
|
1499
|
+
v.literal("pending"),
|
|
1500
|
+
v.literal("running"),
|
|
1501
|
+
v.literal("completed"),
|
|
1502
|
+
v.literal("failed")
|
|
1503
|
+
),
|
|
1504
|
+
|
|
1505
|
+
cursor: v.optional(v.string()),
|
|
1506
|
+
processedCount: v.number(),
|
|
1507
|
+
totalCount: v.optional(v.number()),
|
|
1508
|
+
|
|
1509
|
+
errorMessage: v.optional(v.string()),
|
|
1510
|
+
errorDetails: v.optional(v.any()),
|
|
1511
|
+
|
|
1512
|
+
startedAt: v.number(),
|
|
1513
|
+
completedAt: v.optional(v.number()),
|
|
1514
|
+
})
|
|
1515
|
+
.index("by_status", ["status"])
|
|
1516
|
+
.index("by_entity", ["entityType"]),
|
|
1517
|
+
|
|
1518
|
+
syncConflicts: defineTable({
|
|
1519
|
+
objectId: v.id("objects"),
|
|
1520
|
+
localVersion: v.any(),
|
|
1521
|
+
remoteVersion: v.any(),
|
|
1522
|
+
conflictType: v.string(), // "update_conflict", "delete_conflict"
|
|
1523
|
+
resolvedAt: v.optional(v.number()),
|
|
1524
|
+
resolution: v.optional(v.string()), // "local_wins", "remote_wins", "merged"
|
|
1525
|
+
createdAt: v.number(),
|
|
1526
|
+
})
|
|
1527
|
+
.index("by_object", ["objectId"])
|
|
1528
|
+
.index("by_unresolved", ["resolvedAt"]),
|
|
1529
|
+
});
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
---
|
|
1533
|
+
|
|
1534
|
+
## Notes
|
|
1535
|
+
|
|
1536
|
+
- **MCP is always available** - Even Quick Start users can use Claude Code to customize
|
|
1537
|
+
- **Database is optional** - API-only path doesn't require local DB
|
|
1538
|
+
- **Sync is bidirectional** - Quick Start includes L4YERCAK3 ↔ Local DB sync
|
|
1539
|
+
- **Templates are customizable** - Users can modify generated code
|
|
1540
|
+
- **Type safety throughout** - Full TypeScript support in all paths
|
|
1541
|
+
- **Ontology pattern** - Universal objects table mirrors L4YERCAK3 backend
|
|
1542
|
+
- **Local auth** - OAuth/credentials handled locally, users sync as CRM contacts
|
|
1543
|
+
- **Local Stripe** - Payment processing local, transactions sync to L4YERCAK3
|