gufi-cli 0.1.49 → 0.1.51

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.
Files changed (36) hide show
  1. package/dist/commands/docs.js +1 -5
  2. package/dist/lib/docs-resolver.d.ts +8 -0
  3. package/dist/lib/docs-resolver.js +27 -0
  4. package/dist/lib/security.js +5 -0
  5. package/dist/mcp.js +56 -43
  6. package/docs/dev-guide/1-01-architecture.md +358 -0
  7. package/docs/dev-guide/1-02-multi-tenant.md +415 -0
  8. package/docs/dev-guide/1-03-column-types.md +594 -0
  9. package/docs/dev-guide/1-04-json-config.md +442 -0
  10. package/docs/dev-guide/1-05-authentication.md +427 -0
  11. package/docs/dev-guide/2-01-api-reference.md +564 -0
  12. package/docs/dev-guide/2-02-automations.md +508 -0
  13. package/docs/dev-guide/2-03-gufi-cli.md +568 -0
  14. package/docs/dev-guide/2-04-realtime.md +401 -0
  15. package/docs/dev-guide/2-05-permissions.md +497 -0
  16. package/docs/dev-guide/2-06-integrations-overview.md +104 -0
  17. package/docs/dev-guide/2-07-stripe.md +173 -0
  18. package/docs/dev-guide/2-08-nayax.md +297 -0
  19. package/docs/dev-guide/2-09-ourvend.md +226 -0
  20. package/docs/dev-guide/2-10-tns.md +177 -0
  21. package/docs/dev-guide/2-11-custom-http.md +268 -0
  22. package/docs/dev-guide/3-01-custom-views.md +555 -0
  23. package/docs/dev-guide/3-02-webhooks-api.md +446 -0
  24. package/docs/mcp/00-overview.md +329 -0
  25. package/docs/mcp/01-architecture.md +226 -0
  26. package/docs/mcp/02-modules.md +285 -0
  27. package/docs/mcp/03-fields.md +357 -0
  28. package/docs/mcp/04-views.md +613 -0
  29. package/docs/mcp/05-automations.md +461 -0
  30. package/docs/mcp/06-api.md +531 -0
  31. package/docs/mcp/07-packages.md +246 -0
  32. package/docs/mcp/08-common-errors.md +284 -0
  33. package/docs/mcp/09-examples.md +453 -0
  34. package/docs/mcp/README.md +71 -0
  35. package/docs/mcp/tool-descriptions.json +64 -0
  36. package/package.json +3 -2
@@ -9,12 +9,8 @@
9
9
  */
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
- import { fileURLToPath } from "url";
13
12
  import chalk from "chalk";
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = path.dirname(__filename);
16
- // docs/mcp path relative to CLI
17
- const DOCS_MCP_PATH = path.resolve(__dirname, "../../../../docs/mcp");
13
+ import { DOCS_MCP_PATH } from "../lib/docs-resolver.js";
18
14
  // Topic to file mapping
19
15
  const TOPIC_FILES = {
20
16
  overview: { file: "00-overview.md", description: "Overview and workflow" },
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolves documentation paths for both development (monorepo) and global install.
3
+ *
4
+ * - Global install: docs are bundled at <pkg>/docs/
5
+ * - Development: docs live at <monorepo>/docs/
6
+ */
7
+ export declare const DOCS_MCP_PATH: string;
8
+ export declare const DOCS_DEV_GUIDE_PATH: string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolves documentation paths for both development (monorepo) and global install.
3
+ *
4
+ * - Global install: docs are bundled at <pkg>/docs/
5
+ * - Development: docs live at <monorepo>/docs/
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { fileURLToPath } from "url";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ // Package root: from dist/lib/ go up 2 levels
13
+ const PKG_ROOT = path.resolve(__dirname, "../..");
14
+ function resolveDocsPath(subdir) {
15
+ // 1. Package-local (global install): <pkg>/docs/<subdir>
16
+ const localPath = path.join(PKG_ROOT, "docs", subdir);
17
+ if (fs.existsSync(localPath))
18
+ return localPath;
19
+ // 2. Monorepo (development): <pkg>/../../docs/<subdir>
20
+ const monorepoPath = path.resolve(PKG_ROOT, "../../docs", subdir);
21
+ if (fs.existsSync(monorepoPath))
22
+ return monorepoPath;
23
+ // Fallback to local path (will fail gracefully downstream)
24
+ return localPath;
25
+ }
26
+ export const DOCS_MCP_PATH = resolveDocsPath("mcp");
27
+ export const DOCS_DEV_GUIDE_PATH = resolveDocsPath("dev-guide");
@@ -114,6 +114,11 @@ const ALLOWED_PATTERNS = [
114
114
  // CSRF token reading from cookies - legitimate security practice
115
115
  /csrf-token/i,
116
116
  /getCsrfToken/,
117
+ // Auth cookie management - legitimate for ecommerce auth
118
+ /strong_store_auth/,
119
+ /setAuthCookie/,
120
+ /clearAuthCookie/,
121
+ /document\.cookie/,
117
122
  ];
118
123
  /**
119
124
  * Scan a file for suspicious patterns
package/dist/mcp.js CHANGED
@@ -48,10 +48,8 @@ const COLUMN_TYPE_NAMES = [
48
48
  // For ES modules __dirname equivalent
49
49
  const __filename = fileURLToPath(import.meta.url);
50
50
  const __dirname = path.dirname(__filename);
51
- // docs/mcp path relative to CLI
52
- const DOCS_MCP_PATH = path.resolve(__dirname, "../../../docs/mcp");
53
- // docs/dev-guide path for integration docs
54
- const DOCS_DEV_GUIDE_PATH = path.resolve(__dirname, "../../../docs/dev-guide");
51
+ // Docs paths - resolved for both global install and monorepo dev
52
+ import { DOCS_MCP_PATH, DOCS_DEV_GUIDE_PATH } from "./lib/docs-resolver.js";
55
53
  /**
56
54
  * Parse frontmatter from markdown content
57
55
  */
@@ -502,22 +500,24 @@ const SCHEMA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
502
500
  const PHYSICAL_TABLE_RE = /^m\d+_t\d+$/;
503
501
  /**
504
502
  * Get company schema (cached)
503
+ * Returns { schema, error } - error is set if fetch failed
505
504
  */
506
505
  async function getCompanySchema(companyId, env) {
507
506
  // Include env in cache key to avoid mixing schemas from different environments
508
507
  const cacheKey = `${companyId}:${env || 'prod'}`;
509
508
  const cached = schemaCache.get(cacheKey);
510
509
  if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL) {
511
- return cached.schema;
510
+ return { schema: cached.schema };
512
511
  }
513
512
  try {
514
513
  const data = await apiRequest("/api/cli/schema", {}, companyId, true, env);
515
514
  schemaCache.set(cacheKey, { schema: data, timestamp: Date.now() });
516
- return data;
515
+ return { schema: data };
517
516
  }
518
517
  catch (err) {
519
- console.error(`[MCP] Failed to get schema for company ${companyId}:`, err.message);
520
- return null;
518
+ const errorMsg = err.message;
519
+ console.error(`[MCP] Failed to get schema for company ${companyId}:`, errorMsg);
520
+ return { schema: null, error: errorMsg };
521
521
  }
522
522
  }
523
523
  /**
@@ -538,19 +538,20 @@ async function resolveTableName(tableName, companyId, env) {
538
538
  throw new Error(`Invalid table name: "${tableName}". Use physical (m123_t456) or logical (module.entity) format`);
539
539
  }
540
540
  const [moduleName, entityName] = parts;
541
- const schema = await getCompanySchema(companyId, env);
541
+ const { schema, error } = await getCompanySchema(companyId, env);
542
542
  if (!schema?.modules) {
543
- throw new Error(`Cannot resolve logical table name "${tableName}": schema not available. Use physical name (m123_t456) instead`);
543
+ const reason = error || "unknown error";
544
+ throw new Error(`Cannot resolve logical table name "${tableName}": ${reason}`);
544
545
  }
545
546
  // Find module and entity
546
547
  for (const mod of schema.modules) {
547
548
  const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
548
- mod.label?.toLowerCase() === moduleName.toLowerCase();
549
+ mod.display_name?.toLowerCase() === moduleName.toLowerCase();
549
550
  if (!modMatch)
550
551
  continue;
551
552
  for (const ent of mod.entities || []) {
552
553
  const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
553
- ent.label?.toLowerCase() === entityName.toLowerCase();
554
+ ent.display_name?.toLowerCase() === entityName.toLowerCase();
554
555
  if (entMatch) {
555
556
  // Found! Build physical name from entity ID
556
557
  const moduleId = mod.id;
@@ -856,20 +857,20 @@ Solo Admin y Consultant pueden gestionar endpoints.`,
856
857
  name: "gufi_view_pull",
857
858
  description: `📥 Download view files to local directory for editing.
858
859
 
859
- Downloads to: ~/gufi-dev/view_<id>/
860
+ Downloads to: ~/gufi-dev/company_<id>/view_<id>/
860
861
 
861
862
  After pulling, use Read/Edit tools to work with local files.
862
863
  Then use gufi_view_push to upload changes.
863
864
 
864
- Example: gufi_view_pull({ view_id: 13 })`,
865
+ Example: gufi_view_pull({ view_id: 13, company_id: '150' })`,
865
866
  inputSchema: {
866
867
  type: "object",
867
868
  properties: {
868
869
  view_id: { type: "number", description: "View ID to pull" },
869
- company_id: { type: "string", description: "Company ID (required for company-specific views)" },
870
+ company_id: { type: "string", description: "Company ID (required)" },
870
871
  env: ENV_PARAM,
871
872
  },
872
- required: ["view_id"],
873
+ required: ["view_id", "company_id"],
873
874
  },
874
875
  },
875
876
  // 📤 Push local changes to draft
@@ -1453,9 +1454,9 @@ const toolHandlers = {
1453
1454
  let tableName = null;
1454
1455
  // Resolve entity if provided (optional for standalone scripts)
1455
1456
  if (entity) {
1456
- const schema = await getCompanySchema(company_id, env);
1457
+ const { schema, error } = await getCompanySchema(company_id, env);
1457
1458
  if (!schema?.modules) {
1458
- throw new Error("Cannot get schema. Make sure company_id is correct.");
1459
+ throw new Error(`Cannot get schema: ${error || "unknown error"}`);
1459
1460
  }
1460
1461
  // Parse entity (module.entity format)
1461
1462
  const parts = entity.split(".");
@@ -1466,13 +1467,13 @@ const toolHandlers = {
1466
1467
  // Find module and entity IDs
1467
1468
  for (const mod of schema.modules) {
1468
1469
  const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
1469
- mod.label?.toLowerCase() === moduleName.toLowerCase();
1470
+ mod.display_name?.toLowerCase() === moduleName.toLowerCase();
1470
1471
  if (!modMatch)
1471
1472
  continue;
1472
1473
  moduleId = mod.id;
1473
1474
  for (const ent of mod.entities || []) {
1474
1475
  const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
1475
- ent.label?.toLowerCase() === entityName.toLowerCase();
1476
+ ent.display_name?.toLowerCase() === entityName.toLowerCase();
1476
1477
  if (entMatch) {
1477
1478
  tableId = ent.id;
1478
1479
  tableName = `m${moduleId}_t${tableId}`;
@@ -1785,7 +1786,9 @@ const toolHandlers = {
1785
1786
  // ─────────────────────────────────────────────────────────────────────────
1786
1787
  async gufi_view_pull(params) {
1787
1788
  const viewId = params.view_id;
1788
- const companyId = params.company_id;
1789
+ const companyId = params.company_id != null ? String(params.company_id) : undefined;
1790
+ if (!companyId)
1791
+ throw new Error("company_id is required for view pull");
1789
1792
  const env = params.env;
1790
1793
  const useLocal = canWriteLocal();
1791
1794
  // Get view info
@@ -1798,8 +1801,8 @@ const toolHandlers = {
1798
1801
  const filesResponse = await apiRequest(`/api/marketplace/views/${viewId}/files`, {}, companyId, true, env);
1799
1802
  const files = Array.isArray(filesResponse) ? filesResponse : (filesResponse.data || []);
1800
1803
  if (useLocal) {
1801
- // 💜 CLI: Save to ~/gufi-dev/view_<id>/
1802
- const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1804
+ // 💜 CLI: Save to ~/gufi-dev/company_<id>/view_<id>/
1805
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
1803
1806
  fs.mkdirSync(viewDir, { recursive: true });
1804
1807
  // 💜 Get latest snapshot for version tracking
1805
1808
  let latestSnapshot;
@@ -1823,9 +1826,10 @@ const toolHandlers = {
1823
1826
  const meta = {
1824
1827
  viewId,
1825
1828
  viewName: `view_${viewId}`,
1829
+ company_id: Number(companyId),
1826
1830
  packageId: view.package_id || 0,
1827
1831
  lastSync: new Date().toISOString(),
1828
- lastPulledSnapshot: latestSnapshot, // 💜 For version conflict detection
1832
+ lastPulledSnapshot: latestSnapshot,
1829
1833
  files: fileMeta,
1830
1834
  };
1831
1835
  fs.writeFileSync(path.join(viewDir, ".gufi-view.json"), JSON.stringify(meta, null, 2));
@@ -1837,7 +1841,7 @@ const toolHandlers = {
1837
1841
  local_path: viewDir,
1838
1842
  storage: "local",
1839
1843
  files_count: files.length,
1840
- _hint: `📥 View downloaded to ${viewDir}/. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId}, env: '${env || 'prod'}' }) to upload.`,
1844
+ _hint: `📥 View downloaded to ${viewDir}/. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId}, company_id: '${companyId}' }) to upload.`,
1841
1845
  };
1842
1846
  }
1843
1847
  else {
@@ -1871,33 +1875,42 @@ const toolHandlers = {
1871
1875
  },
1872
1876
  async gufi_view_push(params) {
1873
1877
  const viewId = params.view_id;
1874
- let companyId = params.company_id;
1878
+ let companyId = params.company_id != null ? String(params.company_id) : undefined;
1875
1879
  const env = params.env;
1876
1880
  const useLocal = canWriteLocal();
1881
+ if (!viewId) {
1882
+ throw new Error("view_id is required");
1883
+ }
1877
1884
  // 💜 Try to get company_id from view metadata if not provided
1878
- if (!companyId && viewId && useLocal) {
1879
- const metaPath = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`, ".gufi-view.json");
1880
- if (fs.existsSync(metaPath)) {
1881
- try {
1882
- const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1883
- if (meta.company_id)
1884
- companyId = String(meta.company_id);
1885
+ if (!companyId && useLocal) {
1886
+ const companyDirs = fs.existsSync(LOCAL_VIEWS_DIR)
1887
+ ? fs.readdirSync(LOCAL_VIEWS_DIR).filter(d => d.startsWith("company_"))
1888
+ : [];
1889
+ for (const cd of companyDirs) {
1890
+ const metaPath = path.join(LOCAL_VIEWS_DIR, cd, `view_${viewId}`, ".gufi-view.json");
1891
+ if (fs.existsSync(metaPath)) {
1892
+ try {
1893
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1894
+ if (meta.company_id)
1895
+ companyId = String(meta.company_id);
1896
+ }
1897
+ catch { /* ignore */ }
1898
+ break;
1885
1899
  }
1886
- catch { /* ignore */ }
1887
1900
  }
1888
1901
  }
1889
- if (!viewId) {
1890
- throw new Error("view_id is required");
1902
+ if (!companyId) {
1903
+ throw new Error("company_id is required. Pass it as parameter or pull the view first.");
1891
1904
  }
1892
1905
  let files = [];
1893
- let lastPulledSnapshot; // 💜 For version conflict detection
1906
+ let lastPulledSnapshot;
1894
1907
  if (useLocal) {
1895
- // 💜 CLI: Read from ~/gufi-dev/view_<id>/
1896
- const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1908
+ // 💜 CLI: Read from ~/gufi-dev/company_<id>/view_<id>/
1909
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
1897
1910
  if (!fs.existsSync(viewDir)) {
1898
- throw new Error(`View directory not found: ${viewDir}. Run gufi_view_pull first.`);
1911
+ throw new Error(`View directory not found. Run gufi_view_pull({ view_id: ${viewId}, company_id: '${companyId}' }) first.`);
1899
1912
  }
1900
- // 💜 Read metadata for version tracking (backend handles the check)
1913
+ // 💜 Read metadata for version tracking
1901
1914
  const metaPath = path.join(viewDir, ".gufi-view.json");
1902
1915
  if (fs.existsSync(metaPath)) {
1903
1916
  try {
@@ -2018,7 +2031,7 @@ const toolHandlers = {
2018
2031
  }
2019
2032
  // 💜 Update local metadata with new snapshot number (for version tracking)
2020
2033
  if (useLocal && result.snapshot) {
2021
- const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
2034
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
2022
2035
  const metaPath = path.join(viewDir, ".gufi-view.json");
2023
2036
  if (fs.existsSync(metaPath)) {
2024
2037
  try {
@@ -2556,7 +2569,7 @@ function findTableInModulesMcp(modules, tableName) {
2556
2569
  fields: (ent.fields || []).map((f) => ({
2557
2570
  name: f.name,
2558
2571
  type: f.type,
2559
- label: f.label,
2572
+ display_name: f.display_name,
2560
2573
  required: f.required || false,
2561
2574
  options: f.options?.map((o) => o.value || o),
2562
2575
  })),
@@ -0,0 +1,358 @@
1
+ ---
2
+ id: architecture
3
+ title: "Architecture Overview"
4
+ description: "How Gufi is built and why"
5
+ icon: Layers
6
+ category: dev
7
+ part: 1
8
+ ---
9
+
10
+ # Architecture Overview
11
+
12
+ How Gufi is built and why
13
+
14
+ ## System Architecture
15
+
16
+ Gufi follows a modern microservices architecture optimized for multi-tenancy, real-time updates, and developer experience.
17
+
18
+ ```
19
+ ┌─────────────────────────────────────────────────────────────────┐
20
+ │ CLIENTS │
21
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
22
+ │ │ Web App │ │ Mobile │ │ CLI │ │ API │ │
23
+ │ │ (React) │ │ (PWA) │ │ (Node) │ │ Clients │ │
24
+ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
25
+ │ │ │ │ │ │
26
+ └───────┼─────────────┼─────────────┼─────────────┼───────────────┘
27
+ │ │ │ │
28
+ ▼ ▼ ▼ ▼
29
+ ┌─────────────────────────────────────────────────────────────────┐
30
+ │ LOAD BALANCER │
31
+ └───────────────────────────┬─────────────────────────────────────┘
32
+
33
+ ┌───────────────────┼───────────────────┐
34
+ │ │ │
35
+ ▼ ▼ ▼
36
+ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
37
+ │ Backend ERP │ │ Marketplace │ │ WebSocket │
38
+ │ (Express) │ │ Backend │ │ Server │
39
+ │ :3000 │ │ :3003 │ │ :4000 │
40
+ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
41
+ │ │ │
42
+ └──────────────────┼───────────────────┘
43
+
44
+ ┌──────────────────┴──────────────────┐
45
+ │ │
46
+ ▼ ▼
47
+ ┌───────────────┐ ┌───────────────┐
48
+ │ PostgreSQL │ │ Redis │
49
+ │ (Primary DB) │ │ (Cache) │
50
+ └───────────────┘ └───────────────┘
51
+
52
+
53
+ ┌───────────────┐
54
+ │ Worker │
55
+ │ (pg-boss) │
56
+ └───────────────┘
57
+ ```
58
+
59
+ ## Core Services
60
+
61
+ ### Backend ERP (Port 3000)
62
+
63
+ The main API server handling:
64
+
65
+ - Authentication and authorization
66
+ - CRUD operations for all entities
67
+ - Module and schema management
68
+ - File uploads and storage
69
+ - Business logic and validations
70
+
71
+ **Tech Stack:**
72
+ - Node.js with Express
73
+ - JWT + HttpOnly cookie authentication
74
+ - Zod for validation
75
+ - pg (node-postgres) for database
76
+
77
+ ### Marketplace Backend (Port 3003)
78
+
79
+ Handles marketplace-specific operations:
80
+
81
+ - Package publishing and versioning
82
+ - View code storage and retrieval
83
+ - Developer accounts and billing
84
+ - Cross-company module installations
85
+
86
+ **Key Endpoints:**
87
+ - `/packages` - Package CRUD
88
+ - `/views` - Custom view management
89
+ - `/developer` - Developer portal API
90
+
91
+ ### WebSocket Server (Port 4000)
92
+
93
+ Real-time communication:
94
+
95
+ - LISTEN/NOTIFY from PostgreSQL
96
+ - Broadcasts changes to connected clients
97
+ - Room-based subscriptions per company/table
98
+ - Presence tracking
99
+
100
+ **Protocol:**
101
+ - Native WebSocket (not Socket.IO)
102
+ - JSON message format
103
+ - Heartbeat for connection health
104
+
105
+ ### Worker (pg-boss)
106
+
107
+ Background job processing:
108
+
109
+ - Scheduled automations
110
+ - Email sending
111
+ - Heavy computations
112
+ - Data synchronization
113
+ - External API calls
114
+
115
+ **Features:**
116
+ - Persistent job queue in PostgreSQL
117
+ - Retry with exponential backoff
118
+ - Dead letter queue for failures
119
+ - Monitoring and alerting
120
+
121
+ ## Frontend Architecture
122
+
123
+ ### React Application
124
+
125
+ Built with modern React patterns:
126
+
127
+ ```
128
+ frontend/src/
129
+ ├── features/ # Feature-based organization
130
+ │ ├── auth/ # Login, registration
131
+ │ ├── tables/ # Table views and editing
132
+ │ ├── modules/ # Module management
133
+ │ └── ...
134
+ ├── shared/ # Shared components
135
+ │ ├── ui/ # Base UI components
136
+ │ ├── views/ # View system components
137
+ │ ├── dialogs/ # Dialog components
138
+ │ └── hooks/ # Custom hooks
139
+ ├── stores/ # Zustand state stores
140
+ ├── contexts/ # React contexts
141
+ └── sdk/ # Gufi SDK for views
142
+ ```
143
+
144
+ ### State Management
145
+
146
+ | Store | Purpose |
147
+ |---|---|
148
+ | **authStore** | User session, tokens |
149
+ | **schemaStore** | Company schema cache |
150
+ | **permissionsStore** | Permission cache |
151
+ | **uiStore** | UI state (sidebar, dialogs) |
152
+
153
+ Using Zustand for simple, type-safe state management.
154
+
155
+ ### Data Fetching
156
+
157
+ Using Refine framework:
158
+
159
+ - Automatic CRUD hooks
160
+ - Caching and invalidation
161
+ - Optimistic updates
162
+ - Pagination helpers
163
+
164
+ ## Database Architecture
165
+
166
+ ### PostgreSQL as Core
167
+
168
+ Why PostgreSQL:
169
+
170
+ - JSONB for flexible schemas
171
+ - Row-level security
172
+ - LISTEN/NOTIFY for real-time
173
+ - Excellent performance
174
+ - Strong ACID compliance
175
+
176
+ ### Multi-Tenant Schema
177
+
178
+ ```
179
+ PostgreSQL Instance
180
+ ├── core (schema) # Platform data
181
+ │ ├── users # All users
182
+ │ ├── companies # All companies
183
+ │ ├── modules # Module definitions
184
+ │ ├── entities # Entity definitions
185
+ │ └── automation_scripts # Shared scripts
186
+
187
+ ├── company_1 (schema) # Company 1 data
188
+ │ ├── m1_t1 # Module 1, Entity 1
189
+ │ ├── m1_t2 # Module 1, Entity 2
190
+ │ ├── __audit_log__ # Audit trail
191
+ │ └── ...
192
+
193
+ ├── company_2 (schema) # Company 2 data
194
+ │ └── ...
195
+ └── ...
196
+ ```
197
+
198
+ ### Table Naming Convention
199
+
200
+ Physical tables use IDs:
201
+
202
+ ```
203
+ m{moduleId}_t{entityId}
204
+
205
+ Example: m360_t4589
206
+ └── Module 360, Entity 4589
207
+ ```
208
+
209
+ Logical names (products, customers) map to physical names via schema.
210
+
211
+ ## API Design
212
+
213
+ ### RESTful Endpoints
214
+
215
+ ```
216
+ GET /api/tables/:tableId # List records
217
+ GET /api/tables/:tableId/:rowId # Get record
218
+ POST /api/tables/:tableId # Create record
219
+ PUT /api/tables/:tableId/:rowId # Update record
220
+ DELETE /api/tables/:tableId/:rowId # Delete record
221
+ ```
222
+
223
+ ### Authentication Flow
224
+
225
+ ```
226
+ 1. Login → POST /api/auth/login
227
+ ← Access token (15min) + Refresh cookie (7days)
228
+
229
+ 2. API Request → GET /api/... + Authorization: Bearer {token}
230
+ ← Data response
231
+
232
+ 3. Token Expiring → POST /api/auth/refresh
233
+ ← New access token
234
+ ```
235
+
236
+ ### Response Format
237
+
238
+ Success:
239
+ ```json
240
+ {
241
+ "data": { ... },
242
+ "meta": {
243
+ "total": 100,
244
+ "page": 1,
245
+ "pageSize": 20
246
+ }
247
+ }
248
+ ```
249
+
250
+ Error:
251
+ ```json
252
+ {
253
+ "error": {
254
+ "code": "VALIDATION_ERROR",
255
+ "message": "Price must be positive",
256
+ "details": { "field": "price" }
257
+ }
258
+ }
259
+ ```
260
+
261
+ ## Security Model
262
+
263
+ ### Layers of Security
264
+
265
+ 1. **Network**: HTTPS, WAF, Rate limiting
266
+ 2. **Authentication**: JWT, refresh tokens, 2FA
267
+ 3. **Authorization**: Role-based, entity-level
268
+ 4. **Data**: Row-level security, tenant isolation
269
+ 5. **Audit**: Complete change tracking
270
+
271
+ ### Tenant Isolation
272
+
273
+ Each request:
274
+
275
+ ```
276
+ 1. Extract company_id from JWT
277
+ 2. SET search_path TO company_{id}
278
+ 3. Execute query (sees only company data)
279
+ 4. RESET search_path
280
+ ```
281
+
282
+ No cross-tenant queries possible at database level.
283
+
284
+ ## Caching Strategy
285
+
286
+ ### Redis Cache Layers
287
+
288
+ | Layer | TTL | Purpose |
289
+ |---|---|---|
290
+ | **Schema** | 5 min | Company schemas |
291
+ | **Permissions** | 1 min | User permissions |
292
+ | **Queries** | 30 sec | Expensive queries |
293
+ | **Sessions** | 7 days | User sessions |
294
+
295
+ ### Cache Invalidation
296
+
297
+ Event-driven invalidation:
298
+
299
+ ```
300
+ Record Changed → Invalidate related caches
301
+ → Notify WebSocket
302
+ → Clients refetch
303
+ ```
304
+
305
+ ## Deployment
306
+
307
+ ### Container Architecture
308
+
309
+ ```yaml
310
+ services:
311
+ frontend:
312
+ image: gufi/frontend
313
+ ports: [5173, 8080]
314
+
315
+ backend:
316
+ image: gufi/backend
317
+ ports: [3000]
318
+ depends_on: [db, redis]
319
+
320
+ websocket:
321
+ image: gufi/websocket
322
+ ports: [4000]
323
+ depends_on: [db, redis]
324
+
325
+ worker:
326
+ image: gufi/worker
327
+ depends_on: [db, redis]
328
+
329
+ db:
330
+ image: postgres:15
331
+ volumes: [db_data]
332
+
333
+ redis:
334
+ image: redis:7
335
+ ```
336
+
337
+ ### Scaling
338
+
339
+ | Component | Scale Strategy |
340
+ |---|---|
341
+ | Frontend | Horizontal (CDN + replicas) |
342
+ | Backend | Horizontal (load balanced) |
343
+ | WebSocket | Horizontal (Redis pub/sub) |
344
+ | Worker | Horizontal (pg-boss handles) |
345
+ | PostgreSQL | Vertical + Read replicas |
346
+ | Redis | Cluster mode |
347
+
348
+ ## Performance Targets
349
+
350
+ | Metric | Target |
351
+ |---|---|
352
+ | API Response (P95) | < 100ms |
353
+ | Page Load | < 2s |
354
+ | Time to Interactive | < 3s |
355
+ | WebSocket Latency | < 50ms |
356
+ | Database Query | < 50ms |
357
+
358
+ Monitored continuously with alerting on degradation.