mycontext-cli 2.0.29 → 2.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +194 -61
- package/dist/commands/init.js.map +1 -1
- package/dist/package.json +2 -2
- package/dist/templates/instantdb/db.template.ts +14 -0
- package/dist/templates/instantdb/home-client.template.tsx +127 -0
- package/dist/templates/instantdb/page.template.tsx +5 -0
- package/dist/templates/instantdb/perms.template.ts +9 -0
- package/dist/templates/instantdb/schema.template.ts +28 -0
- package/dist/templates/playbooks/instantdb-integration.md +851 -0
- package/dist/templates/playbooks/mpesa-integration.md +652 -0
- package/dist/templates/pm-integration-config.json +20 -0
- package/dist/templates/ui-spec-examples.md +318 -0
- package/dist/templates/ui-spec-templates.json +244 -0
- package/package.json +2 -2
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: instantdb-integration
|
|
3
|
+
title: InstantDB Integration
|
|
4
|
+
description: Complete InstantDB setup with real-time database, authentication, and React integration
|
|
5
|
+
category: database
|
|
6
|
+
tags: ["instantdb", "realtime", "auth", "react", "nextjs"]
|
|
7
|
+
author: MyContext
|
|
8
|
+
version: 2.0.0
|
|
9
|
+
createdAt: 2025-10-09T10:00:00.000Z
|
|
10
|
+
updatedAt: 2025-10-10T23:30:00.000Z
|
|
11
|
+
difficulty: beginner
|
|
12
|
+
estimatedTime: "5 minutes (automatic) or 2-3 hours (manual)"
|
|
13
|
+
prerequisites: ["Node.js 18+"]
|
|
14
|
+
relatedPlaybooks: ["nextjs-auth", "react-forms", "state-management"]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# InstantDB Integration Guide
|
|
18
|
+
|
|
19
|
+
> ⚡ **Quick Start:** MyContext CLI now handles this automatically! Run:
|
|
20
|
+
> ```bash
|
|
21
|
+
> mycontext init my-app --framework instantdb
|
|
22
|
+
> ```
|
|
23
|
+
> This guide is for manual setup or understanding what MyContext does under the hood.
|
|
24
|
+
|
|
25
|
+
Complete setup for InstantDB real-time database with authentication, schema management, and React integration for Next.js applications.
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
InstantDB provides a real-time database with built-in authentication, making it perfect for modern web applications. This guide covers:
|
|
30
|
+
|
|
31
|
+
- Database schema design and management
|
|
32
|
+
- Magic code authentication flow
|
|
33
|
+
- Real-time data synchronization
|
|
34
|
+
- React hooks and components
|
|
35
|
+
- CRUD operations with TypeScript
|
|
36
|
+
- File storage integration
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
- Next.js 13+ project with App Router
|
|
41
|
+
- MyContext context files (PRD, types, brand)
|
|
42
|
+
- Node.js 18 or higher
|
|
43
|
+
- InstantDB account (free tier available)
|
|
44
|
+
|
|
45
|
+
## Step 1: Installation and Setup
|
|
46
|
+
|
|
47
|
+
### Install Dependencies
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Install InstantDB packages
|
|
51
|
+
pnpm add @instantdb/react @instantdb/admin
|
|
52
|
+
|
|
53
|
+
# Install additional UI dependencies
|
|
54
|
+
pnpm add sonner # for toast notifications
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Environment Configuration
|
|
58
|
+
|
|
59
|
+
Create `.env.local`:
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
NEXT_PUBLIC_INSTANT_APP_ID=your_app_id_here
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Step 2: Schema Design
|
|
66
|
+
|
|
67
|
+
### Create Schema File
|
|
68
|
+
|
|
69
|
+
Create `instant.schema.ts`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { i } from "@instantdb/react";
|
|
73
|
+
|
|
74
|
+
const schema = i.schema({
|
|
75
|
+
entities: {
|
|
76
|
+
// User entities (InstantDB built-in)
|
|
77
|
+
$users: i.entity({
|
|
78
|
+
email: i.string().unique().indexed(),
|
|
79
|
+
name: i.string(),
|
|
80
|
+
createdAt: i.date(),
|
|
81
|
+
updatedAt: i.date(),
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
// Custom entities based on your project
|
|
85
|
+
profiles: i.entity({
|
|
86
|
+
userId: i.string(),
|
|
87
|
+
nickname: i.string(),
|
|
88
|
+
bio: i.string().optional(),
|
|
89
|
+
avatar: i.string().optional(),
|
|
90
|
+
preferences: i.json().optional(),
|
|
91
|
+
createdAt: i.date(),
|
|
92
|
+
updatedAt: i.date(),
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
posts: i.entity({
|
|
96
|
+
title: i.string(),
|
|
97
|
+
content: i.string(),
|
|
98
|
+
authorId: i.string(),
|
|
99
|
+
published: i.boolean(),
|
|
100
|
+
tags: i.json().optional(),
|
|
101
|
+
createdAt: i.date(),
|
|
102
|
+
updatedAt: i.date(),
|
|
103
|
+
}),
|
|
104
|
+
|
|
105
|
+
comments: i.entity({
|
|
106
|
+
postId: i.string(),
|
|
107
|
+
authorId: i.string(),
|
|
108
|
+
content: i.string(),
|
|
109
|
+
parentId: i.string().optional(),
|
|
110
|
+
createdAt: i.date(),
|
|
111
|
+
updatedAt: i.date(),
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
links: {
|
|
116
|
+
// User to profile relationship
|
|
117
|
+
userProfile: {
|
|
118
|
+
forward: { on: "profiles", has: "one", label: "user" },
|
|
119
|
+
reverse: { on: "$users", has: "one", label: "profile" },
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Post to author relationship
|
|
123
|
+
postAuthor: {
|
|
124
|
+
forward: { on: "posts", has: "one", label: "author" },
|
|
125
|
+
reverse: { on: "$users", has: "many", label: "posts" },
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Post to comments relationship
|
|
129
|
+
postComments: {
|
|
130
|
+
forward: { on: "comments", has: "many", label: "post" },
|
|
131
|
+
reverse: { on: "posts", has: "many", label: "comments" },
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Comment to author relationship
|
|
135
|
+
commentAuthor: {
|
|
136
|
+
forward: { on: "comments", has: "one", label: "author" },
|
|
137
|
+
reverse: { on: "$users", has: "many", label: "comments" },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export default schema;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Step 3: Database Client Setup
|
|
146
|
+
|
|
147
|
+
### Create Database Client
|
|
148
|
+
|
|
149
|
+
Create `lib/instantdb.ts`:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { init, id } from "@instantdb/react";
|
|
153
|
+
import schema from "../instant.schema";
|
|
154
|
+
|
|
155
|
+
const db = init({
|
|
156
|
+
appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID || "__APP_ID__",
|
|
157
|
+
schema,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export { db, id };
|
|
161
|
+
|
|
162
|
+
// Auth utilities
|
|
163
|
+
export const authUtils = {
|
|
164
|
+
async sendMagicCode(email: string) {
|
|
165
|
+
try {
|
|
166
|
+
await db.auth.sendMagicCode({ email });
|
|
167
|
+
return { success: true };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { success: false, error: error.message };
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async signInWithMagicCode(email: string, code: string) {
|
|
174
|
+
try {
|
|
175
|
+
await db.auth.signInWithMagicCode({ email, code });
|
|
176
|
+
return { success: true };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return { success: false, error: error.message };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
async signOut() {
|
|
183
|
+
try {
|
|
184
|
+
await db.auth.signOut();
|
|
185
|
+
return { success: true };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return { success: false, error: error.message };
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Generic CRUD operations
|
|
193
|
+
export const dbUtils = {
|
|
194
|
+
async create(collection: string, data: any) {
|
|
195
|
+
try {
|
|
196
|
+
await db.transact([
|
|
197
|
+
db.tx[collection][id()].create({
|
|
198
|
+
...data,
|
|
199
|
+
createdAt: Date.now(),
|
|
200
|
+
updatedAt: Date.now(),
|
|
201
|
+
}),
|
|
202
|
+
]);
|
|
203
|
+
return { success: true };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return { success: false, error: error.message };
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async update(collection: string, id: string, data: any) {
|
|
210
|
+
try {
|
|
211
|
+
await db.transact([
|
|
212
|
+
db.tx[collection][id].update({
|
|
213
|
+
...data,
|
|
214
|
+
updatedAt: Date.now(),
|
|
215
|
+
}),
|
|
216
|
+
]);
|
|
217
|
+
return { success: true };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return { success: false, error: error.message };
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async delete(collection: string, id: string) {
|
|
224
|
+
try {
|
|
225
|
+
await db.transact([db.tx[collection][id].delete()]);
|
|
226
|
+
return { success: true };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { success: false, error: error.message };
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Real-time hooks
|
|
234
|
+
export const useRealtimeQuery = (query: any) => {
|
|
235
|
+
return db.useQuery(query);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const useUser = () => {
|
|
239
|
+
return db.useUser();
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Step 4: Authentication Components
|
|
244
|
+
|
|
245
|
+
### Magic Code Login Form
|
|
246
|
+
|
|
247
|
+
Create `components/auth/LoginForm.tsx`:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
"use client";
|
|
251
|
+
|
|
252
|
+
import { useState } from "react";
|
|
253
|
+
import { Button } from "@/components/ui/button";
|
|
254
|
+
import { Input } from "@/components/ui/input";
|
|
255
|
+
import {
|
|
256
|
+
Card,
|
|
257
|
+
CardContent,
|
|
258
|
+
CardDescription,
|
|
259
|
+
CardHeader,
|
|
260
|
+
CardTitle,
|
|
261
|
+
} from "@/components/ui/card";
|
|
262
|
+
import { authUtils } from "@/lib/instantdb";
|
|
263
|
+
import { toast } from "sonner";
|
|
264
|
+
|
|
265
|
+
interface LoginFormProps {
|
|
266
|
+
onSuccess?: () => void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function LoginForm({ onSuccess }: LoginFormProps) {
|
|
270
|
+
const [email, setEmail] = useState("");
|
|
271
|
+
const [code, setCode] = useState("");
|
|
272
|
+
const [step, setStep] = useState<"email" | "code">("email");
|
|
273
|
+
const [loading, setLoading] = useState(false);
|
|
274
|
+
|
|
275
|
+
const handleSendCode = async (e: React.FormEvent) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
if (!email) return;
|
|
278
|
+
|
|
279
|
+
setLoading(true);
|
|
280
|
+
const result = await authUtils.sendMagicCode(email);
|
|
281
|
+
setLoading(false);
|
|
282
|
+
|
|
283
|
+
if (result.success) {
|
|
284
|
+
setStep("code");
|
|
285
|
+
toast.success("Magic code sent to your email!");
|
|
286
|
+
} else {
|
|
287
|
+
toast.error(result.error || "Failed to send code");
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleVerifyCode = async (e: React.FormEvent) => {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
if (!code) return;
|
|
294
|
+
|
|
295
|
+
setLoading(true);
|
|
296
|
+
const result = await authUtils.signInWithMagicCode(email, code);
|
|
297
|
+
setLoading(false);
|
|
298
|
+
|
|
299
|
+
if (result.success) {
|
|
300
|
+
toast.success("Successfully signed in!");
|
|
301
|
+
onSuccess?.();
|
|
302
|
+
} else {
|
|
303
|
+
toast.error(result.error || "Invalid code");
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<Card className="w-full max-w-md mx-auto">
|
|
309
|
+
<CardHeader>
|
|
310
|
+
<CardTitle>Sign In</CardTitle>
|
|
311
|
+
<CardDescription>
|
|
312
|
+
{step === "email"
|
|
313
|
+
? "Enter your email to receive a magic code"
|
|
314
|
+
: "Enter the code sent to your email"}
|
|
315
|
+
</CardDescription>
|
|
316
|
+
</CardHeader>
|
|
317
|
+
<CardContent>
|
|
318
|
+
{step === "email" ? (
|
|
319
|
+
<form onSubmit={handleSendCode} className="space-y-4">
|
|
320
|
+
<Input
|
|
321
|
+
type="email"
|
|
322
|
+
placeholder="Enter your email"
|
|
323
|
+
value={email}
|
|
324
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
325
|
+
required
|
|
326
|
+
/>
|
|
327
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
328
|
+
{loading ? "Sending..." : "Send Magic Code"}
|
|
329
|
+
</Button>
|
|
330
|
+
</form>
|
|
331
|
+
) : (
|
|
332
|
+
<form onSubmit={handleVerifyCode} className="space-y-4">
|
|
333
|
+
<Input
|
|
334
|
+
type="text"
|
|
335
|
+
placeholder="Enter magic code"
|
|
336
|
+
value={code}
|
|
337
|
+
onChange={(e) => setCode(e.target.value)}
|
|
338
|
+
required
|
|
339
|
+
/>
|
|
340
|
+
<div className="flex gap-2">
|
|
341
|
+
<Button
|
|
342
|
+
type="button"
|
|
343
|
+
variant="outline"
|
|
344
|
+
onClick={() => setStep("email")}
|
|
345
|
+
className="flex-1"
|
|
346
|
+
>
|
|
347
|
+
Back
|
|
348
|
+
</Button>
|
|
349
|
+
<Button type="submit" className="flex-1" disabled={loading}>
|
|
350
|
+
{loading ? "Verifying..." : "Verify Code"}
|
|
351
|
+
</Button>
|
|
352
|
+
</div>
|
|
353
|
+
</form>
|
|
354
|
+
)}
|
|
355
|
+
</CardContent>
|
|
356
|
+
</Card>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### User Dashboard
|
|
362
|
+
|
|
363
|
+
Create `components/auth/UserDashboard.tsx`:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
"use client";
|
|
367
|
+
|
|
368
|
+
import { db } from "@/lib/instantdb";
|
|
369
|
+
import { Button } from "@/components/ui/button";
|
|
370
|
+
import {
|
|
371
|
+
Card,
|
|
372
|
+
CardContent,
|
|
373
|
+
CardDescription,
|
|
374
|
+
CardHeader,
|
|
375
|
+
CardTitle,
|
|
376
|
+
} from "@/components/ui/card";
|
|
377
|
+
import { authUtils } from "@/lib/instantdb";
|
|
378
|
+
import { toast } from "sonner";
|
|
379
|
+
|
|
380
|
+
export function UserDashboard() {
|
|
381
|
+
const user = db.useUser();
|
|
382
|
+
|
|
383
|
+
const handleSignOut = async () => {
|
|
384
|
+
const result = await authUtils.signOut();
|
|
385
|
+
if (result.success) {
|
|
386
|
+
toast.success("Signed out successfully");
|
|
387
|
+
} else {
|
|
388
|
+
toast.error(result.error || "Failed to sign out");
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (!user) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<Card className="w-full max-w-md mx-auto">
|
|
398
|
+
<CardHeader>
|
|
399
|
+
<CardTitle>Welcome, {user.email}!</CardTitle>
|
|
400
|
+
<CardDescription>Your account dashboard</CardDescription>
|
|
401
|
+
</CardHeader>
|
|
402
|
+
<CardContent className="space-y-4">
|
|
403
|
+
<div>
|
|
404
|
+
<p className="text-sm text-muted-foreground">Email:</p>
|
|
405
|
+
<p className="font-medium">{user.email}</p>
|
|
406
|
+
</div>
|
|
407
|
+
<div>
|
|
408
|
+
<p className="text-sm text-muted-foreground">User ID:</p>
|
|
409
|
+
<p className="font-mono text-xs">{user.id}</p>
|
|
410
|
+
</div>
|
|
411
|
+
<Button onClick={handleSignOut} variant="outline" className="w-full">
|
|
412
|
+
Sign Out
|
|
413
|
+
</Button>
|
|
414
|
+
</CardContent>
|
|
415
|
+
</Card>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Auth Provider
|
|
421
|
+
|
|
422
|
+
Create `components/auth/AuthProvider.tsx`:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
"use client";
|
|
426
|
+
|
|
427
|
+
import { db } from "@/lib/instantdb";
|
|
428
|
+
import { LoginForm } from "./LoginForm";
|
|
429
|
+
import { UserDashboard } from "./UserDashboard";
|
|
430
|
+
|
|
431
|
+
export function AuthProvider() {
|
|
432
|
+
return (
|
|
433
|
+
<db.SignedIn>
|
|
434
|
+
<UserDashboard />
|
|
435
|
+
</db.SignedIn>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function AuthForm() {
|
|
440
|
+
return (
|
|
441
|
+
<db.SignedOut>
|
|
442
|
+
<LoginForm />
|
|
443
|
+
</db.SignedOut>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Step 5: Real-time Data Components
|
|
449
|
+
|
|
450
|
+
### Posts List with Real-time Updates
|
|
451
|
+
|
|
452
|
+
Create `components/posts/PostsList.tsx`:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
"use client";
|
|
456
|
+
|
|
457
|
+
import { useRealtimeQuery, dbUtils } from "@/lib/instantdb";
|
|
458
|
+
import { Button } from "@/components/ui/button";
|
|
459
|
+
import {
|
|
460
|
+
Card,
|
|
461
|
+
CardContent,
|
|
462
|
+
CardDescription,
|
|
463
|
+
CardHeader,
|
|
464
|
+
CardTitle,
|
|
465
|
+
} from "@/components/ui/card";
|
|
466
|
+
import { toast } from "sonner";
|
|
467
|
+
|
|
468
|
+
export function PostsList() {
|
|
469
|
+
const { data, isLoading, error } = useRealtimeQuery({
|
|
470
|
+
posts: {
|
|
471
|
+
author: {},
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const handleDeletePost = async (postId: string) => {
|
|
476
|
+
const result = await dbUtils.delete("posts", postId);
|
|
477
|
+
if (result.success) {
|
|
478
|
+
toast.success("Post deleted");
|
|
479
|
+
} else {
|
|
480
|
+
toast.error("Failed to delete post");
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (isLoading) return <div>Loading posts...</div>;
|
|
485
|
+
if (error) return <div>Error loading posts: {error.message}</div>;
|
|
486
|
+
|
|
487
|
+
const posts = data?.posts || [];
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<div className="space-y-4">
|
|
491
|
+
<h2 className="text-2xl font-bold">Posts</h2>
|
|
492
|
+
{posts.length === 0 ? (
|
|
493
|
+
<p className="text-muted-foreground">No posts yet</p>
|
|
494
|
+
) : (
|
|
495
|
+
posts.map((post: any) => (
|
|
496
|
+
<Card key={post.id}>
|
|
497
|
+
<CardHeader>
|
|
498
|
+
<CardTitle>{post.title}</CardTitle>
|
|
499
|
+
<CardDescription>
|
|
500
|
+
By {post.author?.email || "Unknown"} •{" "}
|
|
501
|
+
{new Date(post.createdAt).toLocaleDateString()}
|
|
502
|
+
</CardDescription>
|
|
503
|
+
</CardHeader>
|
|
504
|
+
<CardContent>
|
|
505
|
+
<p className="text-sm">{post.content}</p>
|
|
506
|
+
<div className="mt-4 flex gap-2">
|
|
507
|
+
<Button
|
|
508
|
+
variant="destructive"
|
|
509
|
+
size="sm"
|
|
510
|
+
onClick={() => handleDeletePost(post.id)}
|
|
511
|
+
>
|
|
512
|
+
Delete
|
|
513
|
+
</Button>
|
|
514
|
+
</div>
|
|
515
|
+
</CardContent>
|
|
516
|
+
</Card>
|
|
517
|
+
))
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Create Post Form
|
|
525
|
+
|
|
526
|
+
Create `components/posts/CreatePostForm.tsx`:
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
"use client";
|
|
530
|
+
|
|
531
|
+
import { useState } from "react";
|
|
532
|
+
import { useUser, dbUtils } from "@/lib/instantdb";
|
|
533
|
+
import { Button } from "@/components/ui/button";
|
|
534
|
+
import { Input } from "@/components/ui/input";
|
|
535
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
536
|
+
import {
|
|
537
|
+
Card,
|
|
538
|
+
CardContent,
|
|
539
|
+
CardDescription,
|
|
540
|
+
CardHeader,
|
|
541
|
+
CardTitle,
|
|
542
|
+
} from "@/components/ui/card";
|
|
543
|
+
import { toast } from "sonner";
|
|
544
|
+
|
|
545
|
+
export function CreatePostForm() {
|
|
546
|
+
const user = useUser();
|
|
547
|
+
const [title, setTitle] = useState("");
|
|
548
|
+
const [content, setContent] = useState("");
|
|
549
|
+
const [loading, setLoading] = useState(false);
|
|
550
|
+
|
|
551
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
if (!user || !title || !content) return;
|
|
554
|
+
|
|
555
|
+
setLoading(true);
|
|
556
|
+
const result = await dbUtils.create("posts", {
|
|
557
|
+
title,
|
|
558
|
+
content,
|
|
559
|
+
authorId: user.id,
|
|
560
|
+
published: true,
|
|
561
|
+
});
|
|
562
|
+
setLoading(false);
|
|
563
|
+
|
|
564
|
+
if (result.success) {
|
|
565
|
+
toast.success("Post created successfully!");
|
|
566
|
+
setTitle("");
|
|
567
|
+
setContent("");
|
|
568
|
+
} else {
|
|
569
|
+
toast.error("Failed to create post");
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (!user) {
|
|
574
|
+
return (
|
|
575
|
+
<Card>
|
|
576
|
+
<CardContent className="pt-6">
|
|
577
|
+
<p className="text-muted-foreground">
|
|
578
|
+
Please sign in to create posts
|
|
579
|
+
</p>
|
|
580
|
+
</CardContent>
|
|
581
|
+
</Card>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<Card>
|
|
587
|
+
<CardHeader>
|
|
588
|
+
<CardTitle>Create New Post</CardTitle>
|
|
589
|
+
<CardDescription>
|
|
590
|
+
Share your thoughts with the community
|
|
591
|
+
</CardDescription>
|
|
592
|
+
</CardHeader>
|
|
593
|
+
<CardContent>
|
|
594
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
595
|
+
<Input
|
|
596
|
+
placeholder="Post title"
|
|
597
|
+
value={title}
|
|
598
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
599
|
+
required
|
|
600
|
+
/>
|
|
601
|
+
<Textarea
|
|
602
|
+
placeholder="What's on your mind?"
|
|
603
|
+
value={content}
|
|
604
|
+
onChange={(e) => setContent(e.target.value)}
|
|
605
|
+
rows={4}
|
|
606
|
+
required
|
|
607
|
+
/>
|
|
608
|
+
<Button type="submit" disabled={loading} className="w-full">
|
|
609
|
+
{loading ? "Creating..." : "Create Post"}
|
|
610
|
+
</Button>
|
|
611
|
+
</form>
|
|
612
|
+
</CardContent>
|
|
613
|
+
</Card>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Step 6: File Storage Integration
|
|
619
|
+
|
|
620
|
+
### File Upload Component
|
|
621
|
+
|
|
622
|
+
Create `components/storage/FileUpload.tsx`:
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
"use client";
|
|
626
|
+
|
|
627
|
+
import { useState } from "react";
|
|
628
|
+
import { db } from "@/lib/instantdb";
|
|
629
|
+
import { Button } from "@/components/ui/button";
|
|
630
|
+
import {
|
|
631
|
+
Card,
|
|
632
|
+
CardContent,
|
|
633
|
+
CardDescription,
|
|
634
|
+
CardHeader,
|
|
635
|
+
CardTitle,
|
|
636
|
+
} from "@/components/ui/card";
|
|
637
|
+
import { toast } from "sonner";
|
|
638
|
+
|
|
639
|
+
export function FileUpload() {
|
|
640
|
+
const [file, setFile] = useState<File | null>(null);
|
|
641
|
+
const [uploading, setUploading] = useState(false);
|
|
642
|
+
|
|
643
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
644
|
+
const selectedFile = e.target.files?.[0];
|
|
645
|
+
if (selectedFile) {
|
|
646
|
+
setFile(selectedFile);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const handleUpload = async () => {
|
|
651
|
+
if (!file) return;
|
|
652
|
+
|
|
653
|
+
setUploading(true);
|
|
654
|
+
try {
|
|
655
|
+
const result = await db.storage.uploadFile(file.name, file);
|
|
656
|
+
if (result.data) {
|
|
657
|
+
toast.success("File uploaded successfully!");
|
|
658
|
+
console.log("File ID:", result.data.id);
|
|
659
|
+
// You can now link this file to other entities
|
|
660
|
+
}
|
|
661
|
+
} catch (error) {
|
|
662
|
+
toast.error("Failed to upload file");
|
|
663
|
+
console.error("Upload error:", error);
|
|
664
|
+
} finally {
|
|
665
|
+
setUploading(false);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
return (
|
|
670
|
+
<Card>
|
|
671
|
+
<CardHeader>
|
|
672
|
+
<CardTitle>File Upload</CardTitle>
|
|
673
|
+
<CardDescription>Upload files to InstantDB storage</CardDescription>
|
|
674
|
+
</CardHeader>
|
|
675
|
+
<CardContent className="space-y-4">
|
|
676
|
+
<input
|
|
677
|
+
type="file"
|
|
678
|
+
onChange={handleFileChange}
|
|
679
|
+
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
|
680
|
+
/>
|
|
681
|
+
{file && (
|
|
682
|
+
<div className="text-sm text-muted-foreground">
|
|
683
|
+
Selected: {file.name} ({(file.size / 1024).toFixed(1)} KB)
|
|
684
|
+
</div>
|
|
685
|
+
)}
|
|
686
|
+
<Button
|
|
687
|
+
onClick={handleUpload}
|
|
688
|
+
disabled={!file || uploading}
|
|
689
|
+
className="w-full"
|
|
690
|
+
>
|
|
691
|
+
{uploading ? "Uploading..." : "Upload File"}
|
|
692
|
+
</Button>
|
|
693
|
+
</CardContent>
|
|
694
|
+
</Card>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
## Step 7: App Integration
|
|
700
|
+
|
|
701
|
+
### Main App Layout
|
|
702
|
+
|
|
703
|
+
Update your main page to include authentication and database features:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// app/page.tsx
|
|
707
|
+
import { AuthProvider, AuthForm } from "@/components/auth/AuthProvider";
|
|
708
|
+
import { PostsList } from "@/components/posts/PostsList";
|
|
709
|
+
import { CreatePostForm } from "@/components/posts/CreatePostForm";
|
|
710
|
+
import { FileUpload } from "@/components/storage/FileUpload";
|
|
711
|
+
|
|
712
|
+
export default function HomePage() {
|
|
713
|
+
return (
|
|
714
|
+
<div className="container mx-auto py-8 space-y-8">
|
|
715
|
+
<h1 className="text-4xl font-bold text-center">My App with InstantDB</h1>
|
|
716
|
+
|
|
717
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
718
|
+
<div>
|
|
719
|
+
<AuthProvider />
|
|
720
|
+
<AuthForm />
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div className="space-y-6">
|
|
724
|
+
<CreatePostForm />
|
|
725
|
+
<FileUpload />
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<PostsList />
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
## Step 8: Deployment and Configuration
|
|
736
|
+
|
|
737
|
+
### Push Schema to InstantDB
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
# Install InstantDB CLI
|
|
741
|
+
npm install -g instant-cli
|
|
742
|
+
|
|
743
|
+
# Push your schema
|
|
744
|
+
instant-cli push
|
|
745
|
+
|
|
746
|
+
# Or push just the schema
|
|
747
|
+
instant-cli push schema
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Environment Variables for Production
|
|
751
|
+
|
|
752
|
+
```env
|
|
753
|
+
# .env.production
|
|
754
|
+
NEXT_PUBLIC_INSTANT_APP_ID=your_production_app_id
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## Best Practices
|
|
758
|
+
|
|
759
|
+
### 1. Error Handling
|
|
760
|
+
|
|
761
|
+
Always wrap database operations in try-catch blocks and provide user feedback:
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
const handleCreatePost = async (data: PostData) => {
|
|
765
|
+
try {
|
|
766
|
+
const result = await dbUtils.create("posts", data);
|
|
767
|
+
if (result.success) {
|
|
768
|
+
toast.success("Post created!");
|
|
769
|
+
} else {
|
|
770
|
+
toast.error(result.error || "Failed to create post");
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
toast.error("An unexpected error occurred");
|
|
774
|
+
console.error("Error:", error);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### 2. Optimistic Updates
|
|
780
|
+
|
|
781
|
+
For better UX, update the UI immediately and rollback on error:
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
const handleToggleLike = async (postId: string) => {
|
|
785
|
+
// Optimistic update
|
|
786
|
+
setLiked(!liked);
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
await dbUtils.update("posts", postId, { liked: !liked });
|
|
790
|
+
} catch (error) {
|
|
791
|
+
// Rollback on error
|
|
792
|
+
setLiked(liked);
|
|
793
|
+
toast.error("Failed to update like");
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### 3. Real-time Subscriptions
|
|
799
|
+
|
|
800
|
+
Use specific queries to avoid unnecessary re-renders:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// Good: Specific query
|
|
804
|
+
const { data } = useRealtimeQuery({
|
|
805
|
+
posts: {
|
|
806
|
+
$: { where: { published: true } },
|
|
807
|
+
author: {},
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Avoid: Overly broad queries
|
|
812
|
+
const { data } = useRealtimeQuery({ posts: {} });
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## Troubleshooting
|
|
816
|
+
|
|
817
|
+
### Common Issues
|
|
818
|
+
|
|
819
|
+
1. **Schema not syncing**: Run `instant-cli push` to sync your schema
|
|
820
|
+
2. **Authentication not working**: Check your APP_ID in environment variables
|
|
821
|
+
3. **Real-time updates not working**: Ensure you're using `useRealtimeQuery` for live data
|
|
822
|
+
4. **Type errors**: Make sure your schema types match your TypeScript interfaces
|
|
823
|
+
|
|
824
|
+
### Debug Mode
|
|
825
|
+
|
|
826
|
+
Enable debug mode for development:
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
const db = init({
|
|
830
|
+
appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID,
|
|
831
|
+
schema,
|
|
832
|
+
devtool: true, // Enable InstantDB devtools
|
|
833
|
+
verbose: true, // Enable verbose logging
|
|
834
|
+
});
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
## Next Steps
|
|
838
|
+
|
|
839
|
+
1. **Set up permissions**: Configure `instant.perms.ts` for data access control
|
|
840
|
+
2. **Add more entities**: Extend your schema based on your app's needs
|
|
841
|
+
3. **Implement caching**: Use React Query or SWR for additional caching layers
|
|
842
|
+
4. **Add real-time features**: Implement presence, cursors, and live collaboration
|
|
843
|
+
5. **Optimize performance**: Use pagination and selective queries for large datasets
|
|
844
|
+
|
|
845
|
+
## Resources
|
|
846
|
+
|
|
847
|
+
- [InstantDB Documentation](https://instantdb.com/docs)
|
|
848
|
+
- [Magic Code Authentication](https://instantdb.com/docs/auth/magic-codes)
|
|
849
|
+
- [Real-time Queries](https://instantdb.com/docs/reading-data)
|
|
850
|
+
- [File Storage](https://instantdb.com/docs/storage)
|
|
851
|
+
- [InstantDB CLI](https://instantdb.com/docs/instant-cli)
|