kaddidlehopper 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 (183) hide show
  1. package/CONTEXT.md +139 -0
  2. package/README.md +47 -0
  3. package/add-ons/ai/README.md +34 -0
  4. package/add-ons/ai/assets/_dot_env.local.append +13 -0
  5. package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
  6. package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
  7. package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
  8. package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
  9. package/add-ons/ai/assets/src/routes/chat.css +175 -0
  10. package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
  11. package/add-ons/ai/info.json +27 -0
  12. package/add-ons/ai/package.json +17 -0
  13. package/add-ons/ai/small-logo.svg +8 -0
  14. package/dist/cli.js +251 -0
  15. package/dist/index.js +33 -0
  16. package/dist/types/cli.d.ts +8 -0
  17. package/dist/types/index.d.ts +2 -0
  18. package/dist/types/types.d.ts +14 -0
  19. package/dist/types.js +1 -0
  20. package/examples/blog/README.md +60 -0
  21. package/examples/blog/assets/content/posts/beach.md +12 -0
  22. package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
  23. package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
  24. package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
  25. package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
  26. package/examples/blog/assets/content-collections.ts +30 -0
  27. package/examples/blog/assets/public/beach.jpg +0 -0
  28. package/examples/blog/assets/public/jungle.jpg +0 -0
  29. package/examples/blog/assets/public/mountains.jpg +0 -0
  30. package/examples/blog/assets/public/snorkeling.jpg +0 -0
  31. package/examples/blog/assets/public/waterfall.jpg +0 -0
  32. package/examples/blog/assets/src/components/Header.tsx +52 -0
  33. package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
  34. package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
  35. package/examples/blog/assets/src/components/ui/card.tsx +92 -0
  36. package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
  37. package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
  38. package/examples/blog/assets/src/lib/utils.ts +6 -0
  39. package/examples/blog/assets/src/routes/__root.tsx +57 -0
  40. package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
  41. package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
  42. package/examples/blog/assets/src/routes/index.tsx +19 -0
  43. package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
  44. package/examples/blog/assets/src/styles.css +138 -0
  45. package/examples/blog/info.json +43 -0
  46. package/examples/blog/package.json +23 -0
  47. package/examples/events/README.md +110 -0
  48. package/examples/events/assets/content/speakers/andre-costa.md +22 -0
  49. package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
  50. package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
  51. package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
  52. package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
  53. package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
  54. package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
  55. package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
  56. package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
  57. package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
  58. package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
  59. package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
  60. package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
  61. package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
  62. package/examples/events/assets/content-collections.ts +56 -0
  63. package/examples/events/assets/public/background-1.jpg +0 -0
  64. package/examples/events/assets/public/background-2.jpg +0 -0
  65. package/examples/events/assets/public/background-3.jpg +0 -0
  66. package/examples/events/assets/public/background-4.jpg +0 -0
  67. package/examples/events/assets/public/conference-logo.png +0 -0
  68. package/examples/events/assets/public/favicon.ico +0 -0
  69. package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
  70. package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
  71. package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
  72. package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
  73. package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
  74. package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
  75. package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
  76. package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
  77. package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
  78. package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
  79. package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
  80. package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
  81. package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
  82. package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
  83. package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
  84. package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
  85. package/examples/events/assets/src/components/Header.tsx +59 -0
  86. package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
  87. package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
  88. package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
  89. package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
  90. package/examples/events/assets/src/components/TalkCard.tsx +77 -0
  91. package/examples/events/assets/src/components/ui/card.tsx +92 -0
  92. package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
  93. package/examples/events/assets/src/lib/conference-tools.ts +210 -0
  94. package/examples/events/assets/src/lib/model-selection.ts +1 -0
  95. package/examples/events/assets/src/lib/utils.ts +6 -0
  96. package/examples/events/assets/src/routes/__root.tsx +70 -0
  97. package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
  98. package/examples/events/assets/src/routes/index.tsx +192 -0
  99. package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
  100. package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
  101. package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
  102. package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
  103. package/examples/events/assets/src/routes/talks.index.tsx +40 -0
  104. package/examples/events/assets/src/styles.css +182 -0
  105. package/examples/events/info.json +74 -0
  106. package/examples/events/package.json +23 -0
  107. package/examples/marketing/README.md +60 -0
  108. package/examples/marketing/assets/public/logo.png +0 -0
  109. package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
  110. package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
  111. package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
  112. package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
  113. package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
  114. package/examples/marketing/assets/src/components/Header.tsx +36 -0
  115. package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
  116. package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
  117. package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
  118. package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
  119. package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
  120. package/examples/marketing/assets/src/routes/__root.tsx +57 -0
  121. package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
  122. package/examples/marketing/assets/src/routes/index.tsx +72 -0
  123. package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
  124. package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
  125. package/examples/marketing/assets/src/styles.css +212 -0
  126. package/examples/marketing/info.json +38 -0
  127. package/examples/marketing/package.json +14 -0
  128. package/examples/resume/README.md +82 -0
  129. package/examples/resume/assets/content/education/code-school.md +17 -0
  130. package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
  131. package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
  132. package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
  133. package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
  134. package/examples/resume/assets/content-collections.ts +36 -0
  135. package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
  136. package/examples/resume/assets/src/components/Header.tsx +33 -0
  137. package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
  138. package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
  139. package/examples/resume/assets/src/components/ui/card.tsx +92 -0
  140. package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
  141. package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
  142. package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
  143. package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
  144. package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
  145. package/examples/resume/assets/src/lib/utils.ts +6 -0
  146. package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
  147. package/examples/resume/assets/src/routes/index.tsx +220 -0
  148. package/examples/resume/assets/src/styles.css +138 -0
  149. package/examples/resume/info.json +25 -0
  150. package/examples/resume/package.json +26 -0
  151. package/package.json +39 -0
  152. package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
  153. package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
  154. package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
  155. package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
  156. package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
  157. package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
  158. package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
  159. package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
  160. package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
  161. package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
  162. package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
  163. package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
  164. package/project/base/_dot_gitignore +8 -0
  165. package/project/base/netlify.toml +7 -0
  166. package/project/base/package.json +33 -0
  167. package/project/base/public/favicon.ico +0 -0
  168. package/project/base/public/tanstack-circle-logo.png +0 -0
  169. package/project/base/public/tanstack-word-logo-white.svg +1 -0
  170. package/project/base/src/components/Header.tsx +17 -0
  171. package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
  172. package/project/base/src/router.tsx +15 -0
  173. package/project/base/src/routes/__root.tsx +57 -0
  174. package/project/base/src/routes/index.tsx +48 -0
  175. package/project/base/src/styles.css +15 -0
  176. package/project/base/tsconfig.json +28 -0
  177. package/project/base/vite.config.ts.ejs +25 -0
  178. package/project/packages.json +22 -0
  179. package/scripts/check-outdated-packages.js +421 -0
  180. package/src/cli.ts +343 -0
  181. package/src/index.ts +49 -0
  182. package/src/types.ts +15 -0
  183. package/tsconfig.json +17 -0
@@ -0,0 +1,410 @@
1
+ ---
2
+ name: netlify-blobs
3
+ description: Store and retrieve unstructured data using Netlify Blobs key-value storage. Use for file uploads, caching, user-generated content, session storage, or any binary/JSON data persistence on Netlify.
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: netlify
7
+ version: "1.0"
8
+ ---
9
+
10
+ # Netlify Blobs
11
+
12
+ Netlify Blobs is a built-in key-value store for unstructured data. It's ideal for storing files, JSON, or any binary data without setting up external storage.
13
+
14
+ ## When to Use
15
+
16
+ - Storing user uploads (images, files)
17
+ - Caching API responses
18
+ - Session or state storage
19
+ - Persisting background function results
20
+ - Deploy-specific data storage
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @netlify/blobs
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ ### Writing Data
31
+
32
+ ```typescript
33
+ import { getStore } from "@netlify/blobs";
34
+
35
+ // Get a store reference
36
+ const store = getStore("my-store");
37
+
38
+ // Store a string
39
+ await store.set("greeting", "Hello, World!");
40
+
41
+ // Store JSON (automatically serialized)
42
+ await store.setJSON("user", {
43
+ id: 1,
44
+ name: "Alice",
45
+ email: "alice@example.com"
46
+ });
47
+
48
+ // Store binary data (Buffer, ArrayBuffer, Blob, ReadableStream)
49
+ await store.set("image", imageBuffer);
50
+ ```
51
+
52
+ ### Reading Data
53
+
54
+ ```typescript
55
+ import { getStore } from "@netlify/blobs";
56
+
57
+ const store = getStore("my-store");
58
+
59
+ // Get as string
60
+ const greeting = await store.get("greeting");
61
+ // → "Hello, World!"
62
+
63
+ // Get as JSON (automatically parsed)
64
+ const user = await store.get("user", { type: "json" });
65
+ // → { id: 1, name: "Alice", email: "alice@example.com" }
66
+
67
+ // Get as ArrayBuffer
68
+ const imageData = await store.get("image", { type: "arrayBuffer" });
69
+
70
+ // Get as Blob
71
+ const blob = await store.get("image", { type: "blob" });
72
+
73
+ // Get as Stream
74
+ const stream = await store.get("image", { type: "stream" });
75
+
76
+ // Returns null if key doesn't exist
77
+ const missing = await store.get("nonexistent");
78
+ // → null
79
+ ```
80
+
81
+ ### Deleting Data
82
+
83
+ ```typescript
84
+ const store = getStore("my-store");
85
+
86
+ // Delete a single key
87
+ await store.delete("old-data");
88
+
89
+ // Check if key exists before deleting
90
+ const exists = await store.get("maybe-exists");
91
+ if (exists) {
92
+ await store.delete("maybe-exists");
93
+ }
94
+ ```
95
+
96
+ ### Listing Keys
97
+
98
+ ```typescript
99
+ const store = getStore("my-store");
100
+
101
+ // List all keys
102
+ const { blobs } = await store.list();
103
+ for (const blob of blobs) {
104
+ console.log(blob.key);
105
+ }
106
+
107
+ // List with prefix filter
108
+ const { blobs: userBlobs } = await store.list({ prefix: "users/" });
109
+
110
+ // Paginate through results
111
+ let cursor: string | undefined;
112
+ do {
113
+ const result = await store.list({ cursor });
114
+ for (const blob of result.blobs) {
115
+ console.log(blob.key);
116
+ }
117
+ cursor = result.cursor;
118
+ } while (cursor);
119
+ ```
120
+
121
+ ## Storing Files with Metadata
122
+
123
+ ```typescript
124
+ const store = getStore("uploads");
125
+
126
+ // Store file with metadata
127
+ await store.set("profile-123.jpg", imageBuffer, {
128
+ metadata: {
129
+ contentType: "image/jpeg",
130
+ uploadedBy: "user-123",
131
+ originalName: "my-photo.jpg",
132
+ },
133
+ });
134
+
135
+ // Retrieve with metadata
136
+ const { data, metadata } = await store.getWithMetadata("profile-123.jpg", {
137
+ type: "arrayBuffer",
138
+ });
139
+
140
+ console.log(metadata.contentType); // "image/jpeg"
141
+ ```
142
+
143
+ ## Consistency Modes
144
+
145
+ ### Eventual Consistency (Default)
146
+
147
+ Data is cached at the edge for fast reads. Updates propagate within 60 seconds.
148
+
149
+ ```typescript
150
+ const store = getStore("my-store");
151
+ // Uses eventual consistency by default
152
+ ```
153
+
154
+ ### Strong Consistency
155
+
156
+ For when you need immediate read-after-write consistency:
157
+
158
+ ```typescript
159
+ // Store-level strong consistency
160
+ const store = getStore({
161
+ name: "my-store",
162
+ consistency: "strong",
163
+ });
164
+
165
+ // Or per-operation
166
+ const store = getStore("my-store");
167
+ const data = await store.get("key", { consistency: "strong" });
168
+ ```
169
+
170
+ Use strong consistency when:
171
+ - Reading immediately after writing
172
+ - Handling transactions or counters
173
+ - Data correctness is critical
174
+
175
+ ## Deploy-Scoped Stores
176
+
177
+ Data tied to a specific deploy (cleaned up with deploy):
178
+
179
+ ```typescript
180
+ import { getDeployStore } from "@netlify/blobs";
181
+
182
+ // This store is scoped to the current deploy
183
+ const store = getDeployStore("build-cache");
184
+
185
+ // Data is automatically cleaned up when deploy is deleted
186
+ await store.set("compiled-assets", compiledData);
187
+ ```
188
+
189
+ ## Using in Functions
190
+
191
+ ### Serverless Function Example
192
+
193
+ ```typescript
194
+ // netlify/functions/upload.ts
195
+ import { getStore } from "@netlify/blobs";
196
+ import type { Context } from "@netlify/functions";
197
+
198
+ export default async (request: Request, context: Context) => {
199
+ const store = getStore("uploads");
200
+
201
+ if (request.method === "POST") {
202
+ const formData = await request.formData();
203
+ const file = formData.get("file") as File;
204
+
205
+ if (!file) {
206
+ return new Response("No file provided", { status: 400 });
207
+ }
208
+
209
+ const key = `${Date.now()}-${file.name}`;
210
+ const buffer = await file.arrayBuffer();
211
+
212
+ await store.set(key, buffer, {
213
+ metadata: {
214
+ contentType: file.type,
215
+ originalName: file.name,
216
+ size: file.size.toString(),
217
+ },
218
+ });
219
+
220
+ return Response.json({ key, message: "Upload successful" });
221
+ }
222
+
223
+ if (request.method === "GET") {
224
+ const url = new URL(request.url);
225
+ const key = url.searchParams.get("key");
226
+
227
+ if (!key) {
228
+ return new Response("Key required", { status: 400 });
229
+ }
230
+
231
+ const { data, metadata } = await store.getWithMetadata(key, {
232
+ type: "arrayBuffer",
233
+ });
234
+
235
+ if (!data) {
236
+ return new Response("Not found", { status: 404 });
237
+ }
238
+
239
+ return new Response(data, {
240
+ headers: {
241
+ "Content-Type": metadata?.contentType || "application/octet-stream",
242
+ },
243
+ });
244
+ }
245
+
246
+ return new Response("Method not allowed", { status: 405 });
247
+ };
248
+ ```
249
+
250
+ ### Edge Function Example
251
+
252
+ ```typescript
253
+ // netlify/edge-functions/cache.ts
254
+ import { getStore } from "@netlify/blobs";
255
+
256
+ export default async (request: Request) => {
257
+ const url = new URL(request.url);
258
+ const cacheKey = `page-cache:${url.pathname}`;
259
+
260
+ const store = getStore("page-cache");
261
+
262
+ // Try to get from cache
263
+ const cached = await store.get(cacheKey, { type: "json" });
264
+
265
+ if (cached && cached.expires > Date.now()) {
266
+ return new Response(cached.html, {
267
+ headers: {
268
+ "Content-Type": "text/html",
269
+ "X-Cache": "HIT",
270
+ },
271
+ });
272
+ }
273
+
274
+ // Generate fresh content
275
+ const html = await generatePage(url.pathname);
276
+
277
+ // Cache for 5 minutes
278
+ await store.setJSON(cacheKey, {
279
+ html,
280
+ expires: Date.now() + 5 * 60 * 1000,
281
+ });
282
+
283
+ return new Response(html, {
284
+ headers: {
285
+ "Content-Type": "text/html",
286
+ "X-Cache": "MISS",
287
+ },
288
+ });
289
+ };
290
+ ```
291
+
292
+ ## File-Based Uploads (Build Time)
293
+
294
+ Place files in `.netlify/blobs/deploy` during build:
295
+
296
+ ```
297
+ project/
298
+ ├── .netlify/
299
+ │ └── blobs/
300
+ │ └── deploy/
301
+ │ ├── assets/
302
+ │ │ └── logo.png
303
+ │ ├── $assets/logo.png.json # Metadata file
304
+ │ └── config.json
305
+ ```
306
+
307
+ Metadata file example (`$assets/logo.png.json`):
308
+ ```json
309
+ {
310
+ "contentType": "image/png",
311
+ "uploadedAt": "2024-01-15T10:00:00Z"
312
+ }
313
+ ```
314
+
315
+ ## Common Patterns
316
+
317
+ ### Rate Limiting
318
+
319
+ ```typescript
320
+ const store = getStore({ name: "rate-limits", consistency: "strong" });
321
+
322
+ async function checkRateLimit(ip: string, limit: number, windowMs: number) {
323
+ const key = `rate:${ip}`;
324
+ const now = Date.now();
325
+
326
+ const data = await store.get(key, { type: "json" }) as {
327
+ count: number;
328
+ resetAt: number;
329
+ } | null;
330
+
331
+ if (!data || data.resetAt < now) {
332
+ await store.setJSON(key, { count: 1, resetAt: now + windowMs });
333
+ return { allowed: true, remaining: limit - 1 };
334
+ }
335
+
336
+ if (data.count >= limit) {
337
+ return { allowed: false, remaining: 0, resetAt: data.resetAt };
338
+ }
339
+
340
+ await store.setJSON(key, { count: data.count + 1, resetAt: data.resetAt });
341
+ return { allowed: true, remaining: limit - data.count - 1 };
342
+ }
343
+ ```
344
+
345
+ ### Session Storage
346
+
347
+ ```typescript
348
+ const sessions = getStore({ name: "sessions", consistency: "strong" });
349
+
350
+ async function createSession(userId: string) {
351
+ const sessionId = crypto.randomUUID();
352
+ await sessions.setJSON(`session:${sessionId}`, {
353
+ userId,
354
+ createdAt: Date.now(),
355
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
356
+ });
357
+ return sessionId;
358
+ }
359
+
360
+ async function getSession(sessionId: string) {
361
+ const session = await sessions.get(`session:${sessionId}`, { type: "json" });
362
+ if (!session || session.expiresAt < Date.now()) {
363
+ return null;
364
+ }
365
+ return session;
366
+ }
367
+ ```
368
+
369
+ ### Background Job Results
370
+
371
+ ```typescript
372
+ // Background function writes result
373
+ // netlify/functions/process-background.ts
374
+ import { getStore } from "@netlify/blobs";
375
+
376
+ export default async (request: Request) => {
377
+ const { jobId, data } = await request.json();
378
+ const store = getStore("job-results");
379
+
380
+ await store.setJSON(`job:${jobId}:status`, { status: "processing" });
381
+
382
+ // Do long-running work...
383
+ const result = await processData(data);
384
+
385
+ await store.setJSON(`job:${jobId}:status`, {
386
+ status: "complete",
387
+ result,
388
+ });
389
+ };
390
+
391
+ // Regular function checks status
392
+ // netlify/functions/job-status.ts
393
+ export default async (request: Request) => {
394
+ const url = new URL(request.url);
395
+ const jobId = url.searchParams.get("id");
396
+
397
+ const store = getStore("job-results");
398
+ const status = await store.get(`job:${jobId}:status`, { type: "json" });
399
+
400
+ return Response.json(status || { status: "not-found" });
401
+ };
402
+ ```
403
+
404
+ ## Limits
405
+
406
+ - **Key length**: 600 bytes max
407
+ - **Value size**: 5GB max per blob
408
+ - **Metadata**: 64KB max per blob
409
+ - **Store names**: Must be alphanumeric with hyphens
410
+ - **Consistency**: Eventual by default (60s propagation), strong available