@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.
@@ -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