@codaijs/keel 0.1.0

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 (116) hide show
  1. package/dist/__tests__/cli.test.d.ts +2 -0
  2. package/dist/__tests__/cli.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli.test.js +173 -0
  4. package/dist/__tests__/cli.test.js.map +1 -0
  5. package/dist/__tests__/registry.test.d.ts +2 -0
  6. package/dist/__tests__/registry.test.d.ts.map +1 -0
  7. package/dist/__tests__/registry.test.js +86 -0
  8. package/dist/__tests__/registry.test.js.map +1 -0
  9. package/dist/__tests__/sail-installer.test.d.ts +2 -0
  10. package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
  11. package/dist/__tests__/sail-installer.test.js +158 -0
  12. package/dist/__tests__/sail-installer.test.js.map +1 -0
  13. package/dist/create-runner.d.ts +11 -0
  14. package/dist/create-runner.d.ts.map +1 -0
  15. package/dist/create-runner.js +63 -0
  16. package/dist/create-runner.js.map +1 -0
  17. package/dist/create.d.ts +10 -0
  18. package/dist/create.d.ts.map +1 -0
  19. package/dist/create.js +15 -0
  20. package/dist/create.js.map +1 -0
  21. package/dist/manage.d.ts +24 -0
  22. package/dist/manage.d.ts.map +1 -0
  23. package/dist/manage.js +1461 -0
  24. package/dist/manage.js.map +1 -0
  25. package/dist/prompts.d.ts +36 -0
  26. package/dist/prompts.d.ts.map +1 -0
  27. package/dist/prompts.js +208 -0
  28. package/dist/prompts.js.map +1 -0
  29. package/dist/sail-installer.d.ts +37 -0
  30. package/dist/sail-installer.d.ts.map +1 -0
  31. package/dist/sail-installer.js +935 -0
  32. package/dist/sail-installer.js.map +1 -0
  33. package/dist/scaffold.d.ts +10 -0
  34. package/dist/scaffold.d.ts.map +1 -0
  35. package/dist/scaffold.js +297 -0
  36. package/dist/scaffold.js.map +1 -0
  37. package/package.json +57 -0
  38. package/sails/_template/addon.json +20 -0
  39. package/sails/_template/install.ts +402 -0
  40. package/sails/admin-dashboard/README.md +117 -0
  41. package/sails/admin-dashboard/addon.json +28 -0
  42. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
  43. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
  44. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
  45. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
  46. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
  47. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
  48. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
  49. package/sails/admin-dashboard/install.ts +305 -0
  50. package/sails/analytics/README.md +178 -0
  51. package/sails/analytics/addon.json +27 -0
  52. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
  53. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
  54. package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
  55. package/sails/analytics/install.ts +297 -0
  56. package/sails/file-uploads/README.md +191 -0
  57. package/sails/file-uploads/addon.json +30 -0
  58. package/sails/file-uploads/files/backend/routes/files.ts +198 -0
  59. package/sails/file-uploads/files/backend/schema/files.ts +36 -0
  60. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
  61. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
  62. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
  63. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
  64. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
  65. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
  66. package/sails/file-uploads/install.ts +466 -0
  67. package/sails/gdpr/README.md +174 -0
  68. package/sails/gdpr/addon.json +27 -0
  69. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
  70. package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
  71. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
  72. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
  73. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
  74. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
  75. package/sails/gdpr/install.ts +756 -0
  76. package/sails/google-oauth/README.md +121 -0
  77. package/sails/google-oauth/addon.json +22 -0
  78. package/sails/google-oauth/files/GoogleButton.tsx +50 -0
  79. package/sails/google-oauth/install.ts +252 -0
  80. package/sails/i18n/README.md +193 -0
  81. package/sails/i18n/addon.json +30 -0
  82. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
  83. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
  84. package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
  85. package/sails/i18n/files/frontend/locales/de/common.json +44 -0
  86. package/sails/i18n/files/frontend/locales/en/common.json +44 -0
  87. package/sails/i18n/install.ts +407 -0
  88. package/sails/push-notifications/README.md +163 -0
  89. package/sails/push-notifications/addon.json +31 -0
  90. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
  91. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
  92. package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
  93. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
  94. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
  95. package/sails/push-notifications/install.ts +384 -0
  96. package/sails/r2-storage/README.md +101 -0
  97. package/sails/r2-storage/addon.json +29 -0
  98. package/sails/r2-storage/files/backend/services/storage.ts +71 -0
  99. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
  100. package/sails/r2-storage/install.ts +412 -0
  101. package/sails/rate-limiting/README.md +145 -0
  102. package/sails/rate-limiting/addon.json +20 -0
  103. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
  104. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
  105. package/sails/rate-limiting/install.ts +300 -0
  106. package/sails/registry.json +107 -0
  107. package/sails/stripe/README.md +214 -0
  108. package/sails/stripe/addon.json +24 -0
  109. package/sails/stripe/files/backend/routes/stripe.ts +154 -0
  110. package/sails/stripe/files/backend/schema/stripe.ts +74 -0
  111. package/sails/stripe/files/backend/services/stripe.ts +224 -0
  112. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
  113. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
  114. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
  115. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
  116. package/sails/stripe/install.ts +378 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * PostHog Analytics Sail Installer
3
+ *
4
+ * Adds privacy-friendly analytics with PostHog — automatic page views,
5
+ * user identification, and custom event tracking.
6
+ *
7
+ * PostHog can be used as a cloud service or self-hosted.
8
+ *
9
+ * Usage:
10
+ * npx tsx sails/analytics/install.ts
11
+ */
12
+
13
+ import {
14
+ readFileSync,
15
+ writeFileSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ } from "node:fs";
20
+ import { resolve, dirname, join } from "node:path";
21
+ import { execSync } from "node:child_process";
22
+ import { input, confirm, select } from "@inquirer/prompts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Paths
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
29
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
30
+ const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ interface SailManifest {
37
+ name: string;
38
+ displayName: string;
39
+ version: string;
40
+ requiredEnvVars: { key: string; description: string }[];
41
+ dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
42
+ }
43
+
44
+ function loadManifest(): SailManifest {
45
+ return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
46
+ }
47
+
48
+ function copyFile(src: string, dest: string, label: string): void {
49
+ mkdirSync(dirname(dest), { recursive: true });
50
+ copyFileSync(src, dest);
51
+ console.log(` Copied -> ${label}`);
52
+ }
53
+
54
+ function appendToEnvFiles(section: string, entries: Record<string, string>): void {
55
+ for (const envFile of [".env.example", ".env"]) {
56
+ const envPath = join(PROJECT_ROOT, envFile);
57
+ if (!existsSync(envPath)) continue;
58
+ let content = readFileSync(envPath, "utf-8");
59
+ const lines: string[] = [];
60
+ for (const [key, val] of Object.entries(entries)) {
61
+ if (!content.includes(key)) lines.push(`${key}=${val}`);
62
+ }
63
+ if (lines.length > 0) {
64
+ content += `\n# ${section}\n${lines.join("\n")}\n`;
65
+ writeFileSync(envPath, content, "utf-8");
66
+ console.log(` Updated ${envFile}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ function installDeps(deps: Record<string, string>, workspace: string): void {
72
+ const entries = Object.entries(deps);
73
+ if (entries.length === 0) return;
74
+ const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
75
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
76
+ console.log(` Running: ${cmd}`);
77
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Main
82
+ // ---------------------------------------------------------------------------
83
+
84
+ async function main(): Promise<void> {
85
+ const manifest = loadManifest();
86
+
87
+ // -- Step 1: Welcome --------------------------------------------------------
88
+ console.log("\n------------------------------------------------------------");
89
+ console.log(` PostHog Analytics Sail Installer (v${manifest.version})`);
90
+ console.log("------------------------------------------------------------");
91
+ console.log();
92
+ console.log(" This sail adds PostHog analytics to your app:");
93
+ console.log(" - Automatic page view tracking on SPA route changes");
94
+ console.log(" - User identification tied to your auth system");
95
+ console.log(" - Custom event tracking for feature usage");
96
+ console.log(" - Session recording and heatmaps (configurable in PostHog)");
97
+ console.log();
98
+ console.log(" PostHog is privacy-friendly, open-source, and GDPR-compatible.");
99
+ console.log(" You can use PostHog Cloud or self-host it.");
100
+ console.log();
101
+
102
+ const pkgPath = join(PROJECT_ROOT, "package.json");
103
+ if (existsSync(pkgPath)) {
104
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
105
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
106
+ console.log();
107
+ }
108
+
109
+ // -- Step 2: Hosting choice -------------------------------------------------
110
+ const hostingChoice = await select({
111
+ message: "How will you use PostHog?",
112
+ choices: [
113
+ {
114
+ name: "PostHog Cloud (US)",
115
+ value: "cloud-us",
116
+ description: "Hosted by PostHog at us.i.posthog.com — easiest setup",
117
+ },
118
+ {
119
+ name: "PostHog Cloud (EU)",
120
+ value: "cloud-eu",
121
+ description: "Hosted by PostHog at eu.i.posthog.com — EU data residency",
122
+ },
123
+ {
124
+ name: "Self-hosted",
125
+ value: "self-hosted",
126
+ description: "Your own PostHog instance — full data control",
127
+ },
128
+ ],
129
+ });
130
+
131
+ let posthogHost: string;
132
+
133
+ if (hostingChoice === "cloud-us") {
134
+ posthogHost = "https://us.i.posthog.com";
135
+ console.log();
136
+ console.log(" To get your API key:");
137
+ console.log(" 1. Sign up or log in at https://app.posthog.com");
138
+ console.log(" 2. Create a project (or select existing)");
139
+ console.log(" 3. Go to Project Settings");
140
+ console.log(" 4. Copy the Project API Key");
141
+ console.log();
142
+ } else if (hostingChoice === "cloud-eu") {
143
+ posthogHost = "https://eu.i.posthog.com";
144
+ console.log();
145
+ console.log(" To get your API key:");
146
+ console.log(" 1. Sign up or log in at https://eu.posthog.com");
147
+ console.log(" 2. Create a project (or select existing)");
148
+ console.log(" 3. Go to Project Settings");
149
+ console.log(" 4. Copy the Project API Key");
150
+ console.log();
151
+ } else {
152
+ console.log();
153
+ posthogHost = await input({
154
+ message: "PostHog instance URL (e.g., https://posthog.yourdomain.com):",
155
+ validate: (value) => {
156
+ if (!value || value.trim().length === 0) return "Host URL is required.";
157
+ if (!value.startsWith("http")) return "Should start with http:// or https://";
158
+ return true;
159
+ },
160
+ });
161
+ posthogHost = posthogHost.replace(/\/+$/, ""); // Remove trailing slashes
162
+ console.log();
163
+ }
164
+
165
+ // -- Step 3: Collect API key ------------------------------------------------
166
+ const posthogKey = await input({
167
+ message: "PostHog Project API Key:",
168
+ validate: (value) => {
169
+ if (!value || value.trim().length === 0) return "API key is required.";
170
+ if (value.startsWith("phx_")) return "This looks like a personal API key. Use the Project API key (starts with 'phc_').";
171
+ return true;
172
+ },
173
+ });
174
+
175
+ // -- Step 4: Summary --------------------------------------------------------
176
+ console.log();
177
+ console.log(" Summary of changes:");
178
+ console.log(" -------------------");
179
+ console.log(" Files to create (frontend):");
180
+ console.log(" + packages/frontend/src/lib/analytics.ts");
181
+ console.log(" + packages/frontend/src/hooks/useAnalytics.ts");
182
+ console.log(" + packages/frontend/src/components/AnalyticsProvider.tsx");
183
+ console.log();
184
+ console.log(" Files to modify:");
185
+ console.log(" ~ packages/frontend/src/App.tsx");
186
+ console.log(" ~ .env.example / .env");
187
+ console.log();
188
+ console.log(" Environment variables:");
189
+ console.log(` VITE_POSTHOG_KEY=${posthogKey.slice(0, 12)}...`);
190
+ console.log(` VITE_POSTHOG_HOST=${posthogHost}`);
191
+ console.log();
192
+
193
+ // -- Step 5: Confirm --------------------------------------------------------
194
+ const proceed = await confirm({ message: "Proceed with installation?", default: true });
195
+ if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
196
+
197
+ console.log();
198
+ console.log(" Installing...");
199
+ console.log();
200
+
201
+ // -- Step 6: Copy files and modify App.tsx ----------------------------------
202
+ console.log(" Copying frontend files...");
203
+ const frontendFiles = [
204
+ { src: "frontend/lib/analytics.ts", dest: "src/lib/analytics.ts" },
205
+ { src: "frontend/hooks/useAnalytics.ts", dest: "src/hooks/useAnalytics.ts" },
206
+ { src: "frontend/components/AnalyticsProvider.tsx", dest: "src/components/AnalyticsProvider.tsx" },
207
+ ];
208
+ for (const f of frontendFiles) {
209
+ copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
210
+ }
211
+
212
+ console.log();
213
+ console.log(" Modifying App.tsx...");
214
+
215
+ const appPath = join(FRONTEND_ROOT, "src/App.tsx");
216
+ if (existsSync(appPath)) {
217
+ let appContent = readFileSync(appPath, "utf-8");
218
+
219
+ if (!appContent.includes("AnalyticsProvider")) {
220
+ // Add import
221
+ const importLine = 'import { AnalyticsProvider } from "./components/AnalyticsProvider.js";';
222
+ // Find the last import line and add after it
223
+ const lastImport = appContent.lastIndexOf("import ");
224
+ const importLineEnd = appContent.indexOf("\n", lastImport);
225
+ appContent =
226
+ appContent.slice(0, importLineEnd + 1) +
227
+ importLine + "\n" +
228
+ appContent.slice(importLineEnd + 1);
229
+
230
+ // Wrap <AppRouter /> with <AnalyticsProvider>
231
+ appContent = appContent.replace(
232
+ "<AppRouter />",
233
+ "<AnalyticsProvider>\n <AppRouter />\n </AnalyticsProvider>",
234
+ );
235
+
236
+ writeFileSync(appPath, appContent, "utf-8");
237
+ console.log(" Modified -> App.tsx");
238
+ } else {
239
+ console.log(" Skipped (already present) -> App.tsx");
240
+ }
241
+ } else {
242
+ console.warn(" Warning: App.tsx not found. Wrap your app with <AnalyticsProvider> manually.");
243
+ }
244
+
245
+ console.log();
246
+ console.log(" Installing dependencies...");
247
+ installDeps(manifest.dependencies.frontend, "packages/frontend");
248
+
249
+ console.log();
250
+ console.log(" Updating environment files...");
251
+ appendToEnvFiles("PostHog Analytics", {
252
+ VITE_POSTHOG_KEY: posthogKey.trim(),
253
+ VITE_POSTHOG_HOST: posthogHost,
254
+ });
255
+
256
+ // -- Step 7: Next steps ------------------------------------------------------
257
+ console.log();
258
+ console.log("------------------------------------------------------------");
259
+ console.log(" PostHog Analytics installed successfully!");
260
+ console.log("------------------------------------------------------------");
261
+ console.log();
262
+ console.log(" Next steps:");
263
+ console.log();
264
+ console.log(" 1. Start your dev server:");
265
+ console.log(" npm run dev");
266
+ console.log();
267
+ console.log(" 2. Verify analytics is working:");
268
+ console.log(" - Open your app in the browser");
269
+ console.log(" - Log in and navigate between pages");
270
+ console.log(" - Check PostHog dashboard for events");
271
+ console.log();
272
+ console.log(" 3. Configure PostHog features (optional):");
273
+ console.log(" - Session Recording: enable in PostHog project settings");
274
+ console.log(" - Feature Flags: use posthog.isFeatureEnabled('flag-name')");
275
+ console.log(" - Surveys: create in-app surveys from the PostHog dashboard");
276
+ console.log();
277
+ console.log(" 4. Track custom events in your components:");
278
+ console.log();
279
+ console.log(" import { useAnalytics } from '@/hooks/useAnalytics';");
280
+ console.log();
281
+ console.log(" function MyComponent() {");
282
+ console.log(" const { trackEvent } = useAnalytics();");
283
+ console.log(" return (");
284
+ console.log(" <button onClick={() => trackEvent('button_clicked', { label: 'cta' })}>");
285
+ console.log(" Click me");
286
+ console.log(" </button>");
287
+ console.log(" );");
288
+ console.log(" }");
289
+ console.log();
290
+ console.log(" GDPR note:");
291
+ console.log(" PostHog supports cookie-less tracking and consent management.");
292
+ console.log(" If you have the GDPR sail installed, consider integrating");
293
+ console.log(" analytics consent with your consent tracking system.");
294
+ console.log();
295
+ }
296
+
297
+ main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
@@ -0,0 +1,191 @@
1
+ # File Uploads Sail
2
+
3
+ Generic file upload system with S3-compatible storage. Works with Cloudflare R2, AWS S3, MinIO, and any other S3-compatible provider.
4
+
5
+ ## What this sail adds
6
+
7
+ ### Backend
8
+ - **`src/services/file-storage.ts`** -- S3 client with helpers for presigned URLs, deletion, and listing
9
+ - **`src/routes/files.ts`** -- File management API (all auth-protected):
10
+ - `POST /api/files/upload-url` -- generate a presigned upload URL
11
+ - `GET /api/files` -- list the current user's files
12
+ - `GET /api/files/:fileId` -- get file metadata and download URL
13
+ - `DELETE /api/files/:fileId` -- delete a file
14
+ - **`src/db/schema/files.ts`** -- Drizzle schema for the `files` table
15
+
16
+ ### Frontend
17
+ - **`src/hooks/useFileUpload.ts`** -- React hook for uploading files with progress tracking
18
+ - **`src/hooks/useFiles.ts`** -- React hook for listing, downloading, and deleting files
19
+ - **`src/components/files/FileUploadButton.tsx`** -- Upload button with drag-and-drop support
20
+ - **`src/components/files/FileList.tsx`** -- File browser with download and delete actions
21
+ - **`src/pages/Files.tsx`** -- Full files page combining upload and browser
22
+
23
+ ### Environment variables
24
+
25
+ | Variable | Description | Default |
26
+ |----------|-------------|---------|
27
+ | `S3_ENDPOINT` | S3-compatible endpoint URL | (required) |
28
+ | `S3_ACCESS_KEY_ID` | S3 access key ID | (required) |
29
+ | `S3_SECRET_ACCESS_KEY` | S3 secret access key | (required) |
30
+ | `S3_BUCKET_NAME` | Bucket name | (required) |
31
+ | `S3_PUBLIC_URL` | Public URL for serving files | `""` |
32
+ | `S3_REGION` | S3 region | `auto` |
33
+
34
+ ## Prerequisites
35
+
36
+ 1. An S3-compatible storage account (Cloudflare R2, AWS S3, MinIO, etc.)
37
+ 2. A bucket created in your storage provider
38
+ 3. API credentials with read/write permissions
39
+
40
+ ## Setup
41
+
42
+ ### Run the installer
43
+
44
+ ```bash
45
+ npx tsx cli/sails/file-uploads/install.ts
46
+ ```
47
+
48
+ Or use the CLI:
49
+
50
+ ```bash
51
+ npx @codaijs/keel sail add file-uploads
52
+ ```
53
+
54
+ The installer will guide you through:
55
+ 1. Choosing your storage provider
56
+ 2. Entering credentials
57
+ 3. Configuring max file size
58
+ 4. Copying files and modifying markers
59
+ 5. Installing dependencies and generating migrations
60
+
61
+ ### After installation
62
+
63
+ 1. Run database migrations:
64
+ ```bash
65
+ npm run db:migrate
66
+ ```
67
+
68
+ 2. Configure CORS on your storage bucket (see below)
69
+
70
+ 3. Start the dev server:
71
+ ```bash
72
+ npm run dev
73
+ ```
74
+
75
+ 4. Navigate to `/files` to test
76
+
77
+ ## Upload flow
78
+
79
+ 1. The frontend calls `POST /api/files/upload-url` with the file name and content type
80
+ 2. The backend generates a presigned PUT URL and creates a file record in the database
81
+ 3. The frontend uploads the file directly to storage using the presigned URL
82
+ 4. The file is now tracked in the database and available for listing, downloading, and deletion
83
+
84
+ Files never pass through your server -- they go directly from the browser to storage.
85
+
86
+ ## CORS configuration
87
+
88
+ Your storage bucket must allow PUT and GET requests from your frontend origin.
89
+
90
+ ### Cloudflare R2
91
+
92
+ In your bucket settings, add a CORS policy:
93
+
94
+ ```json
95
+ [
96
+ {
97
+ "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],
98
+ "AllowedMethods": ["GET", "PUT"],
99
+ "AllowedHeaders": ["Content-Type"],
100
+ "MaxAgeSeconds": 3600
101
+ }
102
+ ]
103
+ ```
104
+
105
+ ### AWS S3
106
+
107
+ In bucket permissions, add a CORS configuration:
108
+
109
+ ```json
110
+ [
111
+ {
112
+ "AllowedHeaders": ["Content-Type"],
113
+ "AllowedMethods": ["GET", "PUT"],
114
+ "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],
115
+ "MaxAgeSeconds": 3600
116
+ }
117
+ ]
118
+ ```
119
+
120
+ ### MinIO
121
+
122
+ ```bash
123
+ mc admin config set local api cors_allow_origin=http://localhost:5173
124
+ mc admin service restart local
125
+ ```
126
+
127
+ ## Usage in your own components
128
+
129
+ ### Upload a file
130
+
131
+ ```tsx
132
+ import { useFileUpload } from "@/hooks/useFileUpload";
133
+
134
+ function MyComponent() {
135
+ const { upload, isUploading, progress, error } = useFileUpload();
136
+
137
+ const handleUpload = async (file: File) => {
138
+ const result = await upload(file);
139
+ if (result) {
140
+ console.log("Uploaded:", result.id, result.fileName);
141
+ }
142
+ };
143
+ }
144
+ ```
145
+
146
+ ### List and manage files
147
+
148
+ ```tsx
149
+ import { useFiles } from "@/hooks/useFiles";
150
+
151
+ function MyComponent() {
152
+ const { files, isLoading, deleteFile, getDownloadUrl, refresh } = useFiles();
153
+
154
+ const handleDownload = async (id: string) => {
155
+ const url = await getDownloadUrl(id);
156
+ if (url) window.open(url);
157
+ };
158
+ }
159
+ ```
160
+
161
+ ### Custom upload button
162
+
163
+ ```tsx
164
+ import { FileUploadButton } from "@/components/files/FileUploadButton";
165
+
166
+ <FileUploadButton
167
+ accept="image/*,.pdf"
168
+ maxSize={10 * 1024 * 1024} // 10 MB
169
+ onUploadComplete={(file) => console.log("Uploaded:", file)}
170
+ />
171
+ ```
172
+
173
+ ## Difference from the r2-storage sail
174
+
175
+ The **r2-storage** sail is focused specifically on Cloudflare R2 and profile picture uploads. This **file-uploads** sail is a more general-purpose system:
176
+
177
+ - Works with any S3-compatible provider (R2, S3, MinIO)
178
+ - Supports any file type, not just images
179
+ - Tracks files in a database table
180
+ - Includes a full file browser UI
181
+ - Supports listing, downloading, and deleting files
182
+
183
+ If you only need profile picture uploads with R2, use the r2-storage sail instead.
184
+
185
+ ## Troubleshooting
186
+
187
+ - **CORS errors**: Make sure your bucket CORS policy includes your frontend origin and allows PUT/GET methods
188
+ - **403 Forbidden**: Verify your API credentials have the correct permissions
189
+ - **Upload succeeds but file not in list**: Check the server logs for database insert errors
190
+ - **Presigned URL expired**: URLs expire after 10 minutes -- upload should happen immediately
191
+ - **Large files timing out**: Increase the presigned URL expiry in `file-storage.ts` or your reverse proxy timeout
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "file-uploads",
3
+ "displayName": "File Uploads",
4
+ "description": "Generic file upload system with S3-compatible storage (R2, S3, MinIO). Includes file management API, upload hooks, and a file browser component.",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [
8
+ { "key": "S3_ENDPOINT", "description": "S3-compatible endpoint URL" },
9
+ { "key": "S3_ACCESS_KEY_ID", "description": "S3 access key ID" },
10
+ { "key": "S3_SECRET_ACCESS_KEY", "description": "S3 secret access key" },
11
+ { "key": "S3_BUCKET_NAME", "description": "S3 bucket name" },
12
+ { "key": "S3_PUBLIC_URL", "description": "Public URL for serving files" },
13
+ { "key": "S3_REGION", "description": "S3 region (defaults to auto)" }
14
+ ],
15
+ "dependencies": {
16
+ "backend": {
17
+ "@aws-sdk/client-s3": "^3.700.0",
18
+ "@aws-sdk/s3-request-presigner": "^3.700.0"
19
+ },
20
+ "frontend": {}
21
+ },
22
+ "modifies": {
23
+ "backend": ["src/index.ts", "src/db/schema/index.ts", "src/env.ts"],
24
+ "frontend": ["src/router.tsx"]
25
+ },
26
+ "adds": {
27
+ "backend": ["src/services/file-storage.ts", "src/routes/files.ts", "src/db/schema/files.ts"],
28
+ "frontend": ["src/hooks/useFileUpload.ts", "src/hooks/useFiles.ts", "src/components/files/FileUploadButton.tsx", "src/components/files/FileList.tsx", "src/pages/Files.tsx"]
29
+ }
30
+ }