create-fedi-app 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 (133) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +11113 -0
  3. package/dist/templates/base/.env.example +5 -0
  4. package/dist/templates/base/app/demo/page.tsx +25 -0
  5. package/dist/templates/base/app/globals.css +95 -0
  6. package/dist/templates/base/app/layout.tsx +39 -0
  7. package/dist/templates/base/app/page.tsx +83 -0
  8. package/dist/templates/base/components/FediDevToolbar/FediDevToolbar.tsx +170 -0
  9. package/dist/templates/base/components/providers.tsx +63 -0
  10. package/dist/templates/base/env.ts +10 -0
  11. package/dist/templates/base/hooks/useFediInternal.ts +41 -0
  12. package/dist/templates/base/lib/fedi-types.ts +96 -0
  13. package/dist/templates/base/lib/fedi.ts +18 -0
  14. package/dist/templates/base/lib/nostr/hooks.ts +52 -0
  15. package/dist/templates/base/lib/nostr/index.ts +9 -0
  16. package/dist/templates/base/lib/nostr/mock.ts +60 -0
  17. package/dist/templates/base/lib/nostr/provider.tsx +64 -0
  18. package/dist/templates/base/lib/utils.ts +3 -0
  19. package/dist/templates/base/lib/webln/hooks.ts +67 -0
  20. package/dist/templates/base/lib/webln/index.ts +12 -0
  21. package/dist/templates/base/lib/webln/mock.ts +96 -0
  22. package/dist/templates/base/lib/webln/provider.tsx +52 -0
  23. package/dist/templates/base/next.config.ts +3 -0
  24. package/dist/templates/base/package.json +40 -0
  25. package/dist/templates/base/proxy.ts +8 -0
  26. package/dist/templates/base/tsconfig.json +20 -0
  27. package/dist/templates/base/vitest.config.ts +6 -0
  28. package/dist/templates/base/vitest.setup.ts +40 -0
  29. package/dist/templates/modules/ai-assistant/app/api/assistant/route.ts +45 -0
  30. package/dist/templates/modules/ai-assistant/app/demo/assistant/AssistantDemoClient.tsx +70 -0
  31. package/dist/templates/modules/ai-assistant/app/demo/assistant/page.tsx +23 -0
  32. package/dist/templates/modules/ai-assistant/components/ai/Assistant.tsx +220 -0
  33. package/dist/templates/modules/ai-assistant/components/ai/AssistantProvider.tsx +71 -0
  34. package/dist/templates/modules/ai-assistant/lib/ai/providers.ts +49 -0
  35. package/dist/templates/modules/ai-assistant/module.json +48 -0
  36. package/dist/templates/modules/ai-chat-gated/app/api/chat/invoice/route.ts +15 -0
  37. package/dist/templates/modules/ai-chat-gated/app/api/chat/route.ts +57 -0
  38. package/dist/templates/modules/ai-chat-gated/app/demo/ai-chat/page.tsx +58 -0
  39. package/dist/templates/modules/ai-chat-gated/components/ai/ChatMessage.tsx +50 -0
  40. package/dist/templates/modules/ai-chat-gated/components/ai/GatedChat.tsx +181 -0
  41. package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +168 -0
  42. package/dist/templates/modules/ai-chat-gated/lib/ai/providers.ts +49 -0
  43. package/dist/templates/modules/ai-chat-gated/lib/chat-payment.ts +161 -0
  44. package/dist/templates/modules/ai-chat-gated/module.json +62 -0
  45. package/dist/templates/modules/ai-rules/.cursorrules +8 -0
  46. package/dist/templates/modules/ai-rules/.github/copilot-instructions.md +8 -0
  47. package/dist/templates/modules/ai-rules/CLAUDE.md +8 -0
  48. package/dist/templates/modules/ai-rules/module.json +20 -0
  49. package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +56 -0
  50. package/dist/templates/modules/ai-rules/rules/architecture.md +108 -0
  51. package/dist/templates/modules/ai-rules/rules/design-system.md +94 -0
  52. package/dist/templates/modules/ai-rules/rules/fedi-api.md +120 -0
  53. package/dist/templates/modules/ai-rules/rules/nostr.md +232 -0
  54. package/dist/templates/modules/ai-rules/rules/patterns.md +408 -0
  55. package/dist/templates/modules/ai-rules/rules/testing.md +238 -0
  56. package/dist/templates/modules/ai-rules/rules/webln.md +241 -0
  57. package/dist/templates/modules/database/drizzle/supabase/0000_initial.sql +7 -0
  58. package/dist/templates/modules/database/drizzle/supabase/meta/_journal.json +13 -0
  59. package/dist/templates/modules/database/drizzle/turso/0000_initial.sql +7 -0
  60. package/dist/templates/modules/database/drizzle/turso/meta/_journal.json +13 -0
  61. package/dist/templates/modules/database/drizzle.config.supabase.ts +10 -0
  62. package/dist/templates/modules/database/drizzle.config.turso.ts +11 -0
  63. package/dist/templates/modules/database/env.supabase.ts +24 -0
  64. package/dist/templates/modules/database/env.turso.ts +23 -0
  65. package/dist/templates/modules/database/lib/db/index.supabase.ts +19 -0
  66. package/dist/templates/modules/database/lib/db/index.turso.ts +20 -0
  67. package/dist/templates/modules/database/lib/db/schema.supabase.ts +13 -0
  68. package/dist/templates/modules/database/lib/db/schema.turso.ts +13 -0
  69. package/dist/templates/modules/database/module.json +110 -0
  70. package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +115 -0
  71. package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +162 -0
  72. package/dist/templates/modules/ecash-balance/components/fedi/FediVersionBadge.tsx +39 -0
  73. package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +74 -0
  74. package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +65 -0
  75. package/dist/templates/modules/ecash-balance/module.json +14 -0
  76. package/dist/templates/modules/lnurl/app/api/lnurlauth/route.ts +118 -0
  77. package/dist/templates/modules/lnurl/app/api/lnurlp/[username]/route.ts +70 -0
  78. package/dist/templates/modules/lnurl/app/api/lnurlw/route.ts +57 -0
  79. package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +136 -0
  80. package/dist/templates/modules/lnurl/components/lnurl/LnurlAuth.tsx +156 -0
  81. package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +36 -0
  82. package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +96 -0
  83. package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +141 -0
  84. package/dist/templates/modules/lnurl/lib/lnurl-auth-verify.ts +87 -0
  85. package/dist/templates/modules/lnurl/lib/lnurl-store.ts +112 -0
  86. package/dist/templates/modules/lnurl/lib/lnurl-utils.ts +56 -0
  87. package/dist/templates/modules/lnurl/module.json +27 -0
  88. package/dist/templates/modules/multispend-demo/app/demo/multispend/MultispendDemoClient.tsx +109 -0
  89. package/dist/templates/modules/multispend-demo/app/demo/multispend/page.tsx +23 -0
  90. package/dist/templates/modules/multispend-demo/components/multispend/ApprovalVote.tsx +122 -0
  91. package/dist/templates/modules/multispend-demo/components/multispend/MultispendDemo.tsx +220 -0
  92. package/dist/templates/modules/multispend-demo/components/multispend/MultispendProposal.tsx +213 -0
  93. package/dist/templates/modules/multispend-demo/components/multispend/ProposalList.tsx +49 -0
  94. package/dist/templates/modules/multispend-demo/hooks/useMultispendDemo.ts +127 -0
  95. package/dist/templates/modules/multispend-demo/lib/multispend-types.ts +33 -0
  96. package/dist/templates/modules/multispend-demo/lib/multispend-utils.ts +69 -0
  97. package/dist/templates/modules/multispend-demo/module.json +18 -0
  98. package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/NostrFeedDemoClient.tsx +134 -0
  99. package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/page.tsx +23 -0
  100. package/dist/templates/modules/nostr-feed/components/nostr/NostrFeedProvider.tsx +47 -0
  101. package/dist/templates/modules/nostr-feed/components/nostr/NoteCard.tsx +68 -0
  102. package/dist/templates/modules/nostr-feed/components/nostr/NoteFeed.tsx +109 -0
  103. package/dist/templates/modules/nostr-feed/components/nostr/PublishNote.tsx +104 -0
  104. package/dist/templates/modules/nostr-feed/components/nostr/ZapButton.tsx +140 -0
  105. package/dist/templates/modules/nostr-feed/lib/nostr/relay.ts +107 -0
  106. package/dist/templates/modules/nostr-feed/lib/nostr-zap.ts +159 -0
  107. package/dist/templates/modules/nostr-feed/module.json +25 -0
  108. package/dist/templates/modules/nostr-identity/app/demo/nostr/page.tsx +136 -0
  109. package/dist/templates/modules/nostr-identity/components/nostr/IdentityBadge.tsx +109 -0
  110. package/dist/templates/modules/nostr-identity/components/nostr/NostrLogin.tsx +107 -0
  111. package/dist/templates/modules/nostr-identity/components/nostr/SignedMessage.tsx +103 -0
  112. package/dist/templates/modules/nostr-identity/hooks/useIdentityFlow.ts +61 -0
  113. package/dist/templates/modules/nostr-identity/lib/nostr-utils.ts +30 -0
  114. package/dist/templates/modules/nostr-identity/module.json +15 -0
  115. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +25 -0
  116. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/verify/route.ts +39 -0
  117. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +71 -0
  118. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +134 -0
  119. package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +267 -0
  120. package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +195 -0
  121. package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +104 -0
  122. package/dist/templates/modules/payment-gated-content/module.json +24 -0
  123. package/dist/templates/modules/payment-gated-content/proxy.ts +27 -0
  124. package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +176 -0
  125. package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +170 -0
  126. package/dist/templates/modules/webln-payments/components/webln/PayButton.tsx +92 -0
  127. package/dist/templates/modules/webln-payments/components/webln/PaymentHistory.tsx +102 -0
  128. package/dist/templates/modules/webln-payments/hooks/__tests__/usePaymentFlow.test.tsx +182 -0
  129. package/dist/templates/modules/webln-payments/hooks/usePaymentFlow.ts +100 -0
  130. package/dist/templates/modules/webln-payments/lib/payment-history.ts +75 -0
  131. package/dist/templates/modules/webln-payments/module.json +17 -0
  132. package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +41 -0
  133. package/package.json +29 -0
@@ -0,0 +1,241 @@
1
+ # WebLN Reference
2
+
3
+ WebLN is a JavaScript standard for browser-based Lightning wallet access. In Fedi, `window.webln` is injected by the native app into every Mini App's WebView.
4
+
5
+ ## How Fedi injects window.webln
6
+
7
+ Fedi's native app injects a `WebLNProvider` implementation into `window.webln` before the page loads. This provider routes calls through the native Lightning node (Fedimint eCash). No installation, no permissions dialog — it's there when the app opens inside Fedi.
8
+
9
+ Outside Fedi (regular browser, SSR, tests without mocks) `window.webln` is `undefined`.
10
+
11
+ ## Detection pattern
12
+
13
+ Always check before use. Never assume availability.
14
+
15
+ ```ts
16
+ if (typeof window.webln !== 'undefined') {
17
+ // safe to use webln
18
+ } else {
19
+ // show fallback UI or message
20
+ }
21
+ ```
22
+
23
+ The `@create-fedi-app/webln` package handles this internally — prefer using hooks over direct `window.webln` calls.
24
+
25
+ ## React hooks (preferred)
26
+
27
+ ### useWebLN()
28
+
29
+ Returns the active provider and connection state.
30
+
31
+ ```ts
32
+ import { useWebLN } from '@create-fedi-app/webln';
33
+
34
+ const { provider, isLoading, error, isConnected } = useWebLN();
35
+ ```
36
+
37
+ | Field | Type | Description |
38
+ |-------|------|-------------|
39
+ | `provider` | `WebLNProvider \| null` | The active provider, or null if not connected |
40
+ | `isLoading` | `boolean` | True while the provider is initialising |
41
+ | `error` | `Error \| null` | Set if provider initialisation failed |
42
+ | `isConnected` | `boolean` | Shorthand for `provider !== null` |
43
+
44
+ ### usePayment()
45
+
46
+ State-managed wrappers for `sendPayment` and `makeInvoice`.
47
+
48
+ ```ts
49
+ import { usePayment } from '@create-fedi-app/webln';
50
+
51
+ const {
52
+ sendPayment,
53
+ makeInvoice,
54
+ isPaying,
55
+ isCreatingInvoice,
56
+ paymentError,
57
+ lastPreimage,
58
+ lastInvoice,
59
+ } = usePayment();
60
+ ```
61
+
62
+ | Field | Type | Description |
63
+ |-------|------|-------------|
64
+ | `sendPayment(invoice)` | `(str) => Promise<SendPaymentResponse \| null>` | Pay a BOLT11 invoice |
65
+ | `makeInvoice(args)` | `(args) => Promise<RequestInvoiceResponse \| null>` | Create a receive invoice |
66
+ | `isPaying` | `boolean` | True during `sendPayment` |
67
+ | `isCreatingInvoice` | `boolean` | True during `makeInvoice` |
68
+ | `paymentError` | `Error \| null` | Last error from either call |
69
+ | `lastPreimage` | `string \| null` | Preimage from last successful payment |
70
+ | `lastInvoice` | `string \| null` | Payment request from last created invoice |
71
+
72
+ ## Full API (window.webln)
73
+
74
+ ### enable()
75
+
76
+ ```ts
77
+ await window.webln.enable(): Promise<void>
78
+ ```
79
+
80
+ Requests wallet access. **You do not need to call this in Fedi** — the `@create-fedi-app/webln` provider calls it automatically during initialisation.
81
+
82
+ ### getInfo()
83
+
84
+ ```ts
85
+ const info = await window.webln.getInfo(): Promise<GetInfoResponse>
86
+ ```
87
+
88
+ ```ts
89
+ interface GetInfoResponse {
90
+ node: {
91
+ alias: string; // e.g. "Fedi Dev Node"
92
+ pubkey: string; // hex-encoded node public key
93
+ color: string; // hex color, e.g. "#FF6B35"
94
+ };
95
+ methods: string[]; // e.g. ["sendPayment", "makeInvoice", "getInfo", "signMessage"]
96
+ }
97
+ ```
98
+
99
+ ### sendPayment()
100
+
101
+ ```ts
102
+ const result = await window.webln.sendPayment(paymentRequest: string): Promise<SendPaymentResponse>
103
+ ```
104
+
105
+ `paymentRequest` must be a BOLT11 string starting with `lnbc`.
106
+
107
+ ```ts
108
+ interface SendPaymentResponse {
109
+ preimage: string; // hex-encoded 32-byte payment preimage — proof of payment
110
+ }
111
+ ```
112
+
113
+ Throws on failure (insufficient funds, expired invoice, routing failure, etc.).
114
+
115
+ ### makeInvoice()
116
+
117
+ ```ts
118
+ const result = await window.webln.makeInvoice(args): Promise<RequestInvoiceResponse>
119
+ ```
120
+
121
+ ```ts
122
+ // Three calling conventions — all valid:
123
+ makeInvoice(1000) // exact sats
124
+ makeInvoice('1000') // exact sats as string
125
+ makeInvoice({ amount: '1000', defaultMemo: 'Coffee' }) // full args
126
+
127
+ interface RequestInvoiceArgs {
128
+ amount?: string | number; // exact amount in sats
129
+ defaultAmount?: string | number; // suggested amount (user can change)
130
+ minimumAmount?: string | number;
131
+ maximumAmount?: string | number;
132
+ defaultMemo?: string;
133
+ }
134
+
135
+ interface RequestInvoiceResponse {
136
+ paymentRequest: string; // BOLT11 string
137
+ }
138
+ ```
139
+
140
+ ### signMessage()
141
+
142
+ ```ts
143
+ const result = await window.webln.signMessage(message: string): Promise<SignMessageResponse>
144
+
145
+ interface SignMessageResponse {
146
+ message: string; // the original message
147
+ signature: string; // hex-encoded signature
148
+ }
149
+ ```
150
+
151
+ Produces a Lightning node signature. Useful for proving Lightning identity. Not the same as Nostr signing.
152
+
153
+ ### sendKeysend()
154
+
155
+ ```ts
156
+ const result = await window.webln.sendKeysend(args: KeysendArgs): Promise<SendPaymentResponse>
157
+
158
+ interface KeysendArgs {
159
+ destination: string; // destination node pubkey
160
+ amount: string | number; // sats
161
+ customRecords?: Record<string, string>; // TLV records
162
+ }
163
+ ```
164
+
165
+ ### verifyMessage()
166
+
167
+ ```ts
168
+ await window.webln.verifyMessage(signature: string, message: string): Promise<void>
169
+ ```
170
+
171
+ Verifies a signature produced by `signMessage`. Throws if invalid.
172
+
173
+ ## Common errors
174
+
175
+ | Error message | Cause | Fix |
176
+ |--------------|-------|-----|
177
+ | `"Payment failed"` | Generic payment failure | Show user-friendly error, offer retry |
178
+ | `"Invalid payment request: must start with lnbc"` | Bad invoice string | Validate invoice format before calling |
179
+ | `"Insufficient funds"` | Wallet balance too low | Show balance, suggest amount |
180
+ | `"Invoice expired"` | BOLT11 invoice past expiry | Refresh invoice and retry |
181
+
182
+ ## Graceful fallback pattern
183
+
184
+ ```tsx
185
+ 'use client';
186
+ import { useWebLN } from '@create-fedi-app/webln';
187
+
188
+ export function PaymentSection() {
189
+ const { isConnected, isLoading } = useWebLN();
190
+
191
+ if (isLoading) return <p>Connecting to wallet…</p>;
192
+
193
+ if (!isConnected) {
194
+ return (
195
+ <div>
196
+ <p>Open this app inside Fedi to make payments.</p>
197
+ </div>
198
+ );
199
+ }
200
+
201
+ return <YourPaymentUI />;
202
+ }
203
+ ```
204
+
205
+ ## MockWebLNProvider (tests)
206
+
207
+ ```ts
208
+ import { MockWebLNProvider } from '@create-fedi-app/webln';
209
+
210
+ // Default: succeeds with 600ms delay
211
+ const mock = new MockWebLNProvider();
212
+
213
+ // Custom options:
214
+ const slowMock = new MockWebLNProvider({ paymentDelay: 2000 });
215
+ const failMock = new MockWebLNProvider({
216
+ shouldFail: true,
217
+ failureMessage: 'Simulated failure',
218
+ });
219
+ ```
220
+
221
+ `MockWebLNProvider` implements the full `WebLNProvider` interface:
222
+ - `enable()` — no-op (throws if `shouldFail: true`)
223
+ - `getInfo()` — returns static Fedi Dev Node info
224
+ - `makeInvoice(args)` — returns a fake `lnbc...` string
225
+ - `sendPayment(invoice)` — validates `lnbc` prefix, returns a random preimage hex
226
+ - `signMessage(msg)` — returns message + random hex signature
227
+ - `sendKeysend(args)` — returns a random preimage hex
228
+ - `verifyMessage()` — no-op
229
+
230
+ Use it in Vitest tests by passing it to the `WebLNProvider` context:
231
+
232
+ ```tsx
233
+ import { WebLNProvider } from '@create-fedi-app/webln';
234
+ import { MockWebLNProvider } from '@create-fedi-app/webln';
235
+
236
+ render(
237
+ <WebLNProvider mock={new MockWebLNProvider()}>
238
+ <YourComponent />
239
+ </WebLNProvider>
240
+ );
241
+ ```
@@ -0,0 +1,7 @@
1
+ CREATE TABLE "payments" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "invoice" text NOT NULL,
4
+ "preimage" text NOT NULL,
5
+ "paid_at" timestamp with time zone,
6
+ "metadata" jsonb
7
+ );
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1748736000000,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `payments` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `invoice` text NOT NULL,
4
+ `preimage` text NOT NULL,
5
+ `paid_at` integer,
6
+ `metadata` text
7
+ );
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1748736000000,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './lib/db/schema.ts',
5
+ out: './drizzle',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './lib/db/schema.ts',
5
+ out: './drizzle',
6
+ dialect: 'turso',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ authToken: process.env.DATABASE_AUTH_TOKEN,
10
+ },
11
+ });
@@ -0,0 +1,24 @@
1
+ import { createEnv } from '@t3-oss/env-nextjs';
2
+ import { z } from 'zod';
3
+
4
+ export const env = createEnv({
5
+ server: {
6
+ NODE_ENV: z.enum(['development', 'production', 'test']),
7
+ DATABASE_URL: z
8
+ .string()
9
+ .min(1)
10
+ .refine(
11
+ (value) =>
12
+ value.startsWith('postgresql://') || value.startsWith('postgres://'),
13
+ 'DATABASE_URL must be a postgresql:// URL for Supabase',
14
+ ),
15
+ DATABASE_AUTH_TOKEN: z.string().optional(),
16
+ },
17
+ client: { NEXT_PUBLIC_APP_NAME: z.string().default('My Fedi App') },
18
+ runtimeEnv: {
19
+ NODE_ENV: process.env.NODE_ENV,
20
+ DATABASE_URL: process.env.DATABASE_URL,
21
+ DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN,
22
+ NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
23
+ },
24
+ });
@@ -0,0 +1,23 @@
1
+ import { createEnv } from '@t3-oss/env-nextjs';
2
+ import { z } from 'zod';
3
+
4
+ export const env = createEnv({
5
+ server: {
6
+ NODE_ENV: z.enum(['development', 'production', 'test']),
7
+ DATABASE_URL: z
8
+ .string()
9
+ .min(1)
10
+ .refine(
11
+ (value) => value.startsWith('libsql://') || value.startsWith('file:'),
12
+ 'DATABASE_URL must be a libsql:// or file: URL for Turso',
13
+ ),
14
+ DATABASE_AUTH_TOKEN: z.string().min(1),
15
+ },
16
+ client: { NEXT_PUBLIC_APP_NAME: z.string().default('My Fedi App') },
17
+ runtimeEnv: {
18
+ NODE_ENV: process.env.NODE_ENV,
19
+ DATABASE_URL: process.env.DATABASE_URL,
20
+ DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN,
21
+ NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
22
+ },
23
+ });
@@ -0,0 +1,19 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import postgres from 'postgres';
3
+ import { env } from '../../env';
4
+ import * as schema from './schema';
5
+
6
+ export { createClient } from '@supabase/supabase-js';
7
+
8
+ const url = env.DATABASE_URL;
9
+
10
+ if (!url.startsWith('postgresql://') && !url.startsWith('postgres://')) {
11
+ throw new Error(
12
+ 'DATABASE_URL must be a postgresql:// URL for Supabase. Regenerate with database=supabase or update your connection string.',
13
+ );
14
+ }
15
+
16
+ const client = postgres(url, { prepare: false });
17
+
18
+ export const db = drizzle(client, { schema });
19
+ export * from './schema';
@@ -0,0 +1,20 @@
1
+ import { createClient } from '@libsql/client';
2
+ import { drizzle } from 'drizzle-orm/libsql';
3
+ import { env } from '../../env';
4
+ import * as schema from './schema';
5
+
6
+ const url = env.DATABASE_URL;
7
+
8
+ if (!url.startsWith('libsql://') && !url.startsWith('file:')) {
9
+ throw new Error(
10
+ 'DATABASE_URL must be a libsql:// or file: URL for Turso. Regenerate with database=turso or update your connection string.',
11
+ );
12
+ }
13
+
14
+ const client = createClient({
15
+ url,
16
+ authToken: env.DATABASE_AUTH_TOKEN,
17
+ });
18
+
19
+ export const db = drizzle(client, { schema });
20
+ export * from './schema';
@@ -0,0 +1,13 @@
1
+ import { jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ /** Payment records for payment-gated content and other Lightning-gated features. */
4
+ export const payments = pgTable('payments', {
5
+ id: text('id').primaryKey(),
6
+ invoice: text('invoice').notNull(),
7
+ preimage: text('preimage').notNull(),
8
+ paidAt: timestamp('paid_at', { withTimezone: true, mode: 'date' }),
9
+ metadata: jsonb('metadata').$type<Record<string, unknown>>(),
10
+ });
11
+
12
+ export type TPayment = typeof payments.$inferSelect;
13
+ export type TNewPayment = typeof payments.$inferInsert;
@@ -0,0 +1,13 @@
1
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+
3
+ /** Payment records for payment-gated content and other Lightning-gated features. */
4
+ export const payments = sqliteTable('payments', {
5
+ id: text('id').primaryKey(),
6
+ invoice: text('invoice').notNull(),
7
+ preimage: text('preimage').notNull(),
8
+ paidAt: integer('paid_at', { mode: 'timestamp_ms' }),
9
+ metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>(),
10
+ });
11
+
12
+ export type TPayment = typeof payments.$inferSelect;
13
+ export type TNewPayment = typeof payments.$inferInsert;
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "database",
3
+ "description": "Drizzle ORM CRUD example with Turso or Supabase",
4
+ "dependencies": ["drizzle-orm"],
5
+ "devDependencies": ["drizzle-kit"],
6
+ "databaseType": {
7
+ "turso": {
8
+ "dependencies": ["@libsql/client"],
9
+ "devDependencies": []
10
+ },
11
+ "supabase": {
12
+ "dependencies": ["postgres", "@supabase/supabase-js"],
13
+ "devDependencies": []
14
+ }
15
+ },
16
+ "scripts": {
17
+ "db:generate": "drizzle-kit generate",
18
+ "db:migrate": "drizzle-kit migrate",
19
+ "db:push": "drizzle-kit push",
20
+ "db:studio": "drizzle-kit studio"
21
+ },
22
+ "files": [
23
+ {
24
+ "src": "lib/db/index.turso.ts",
25
+ "dest": "lib/db/index.ts",
26
+ "merge": "add",
27
+ "databaseType": "turso"
28
+ },
29
+ {
30
+ "src": "lib/db/index.supabase.ts",
31
+ "dest": "lib/db/index.ts",
32
+ "merge": "add",
33
+ "databaseType": "supabase"
34
+ },
35
+ {
36
+ "src": "lib/db/schema.turso.ts",
37
+ "dest": "lib/db/schema.ts",
38
+ "merge": "add",
39
+ "databaseType": "turso"
40
+ },
41
+ {
42
+ "src": "lib/db/schema.supabase.ts",
43
+ "dest": "lib/db/schema.ts",
44
+ "merge": "add",
45
+ "databaseType": "supabase"
46
+ },
47
+ {
48
+ "src": "drizzle.config.turso.ts",
49
+ "dest": "drizzle.config.ts",
50
+ "merge": "add",
51
+ "databaseType": "turso"
52
+ },
53
+ {
54
+ "src": "drizzle.config.supabase.ts",
55
+ "dest": "drizzle.config.ts",
56
+ "merge": "add",
57
+ "databaseType": "supabase"
58
+ },
59
+ {
60
+ "src": "env.turso.ts",
61
+ "dest": "env.ts",
62
+ "merge": "replace",
63
+ "databaseType": "turso"
64
+ },
65
+ {
66
+ "src": "env.supabase.ts",
67
+ "dest": "env.ts",
68
+ "merge": "replace",
69
+ "databaseType": "supabase"
70
+ },
71
+ {
72
+ "src": "drizzle/turso/0000_initial.sql",
73
+ "dest": "drizzle/0000_initial.sql",
74
+ "merge": "add",
75
+ "databaseType": "turso"
76
+ },
77
+ {
78
+ "src": "drizzle/turso/meta/_journal.json",
79
+ "dest": "drizzle/meta/_journal.json",
80
+ "merge": "add",
81
+ "databaseType": "turso"
82
+ },
83
+ {
84
+ "src": "drizzle/supabase/0000_initial.sql",
85
+ "dest": "drizzle/0000_initial.sql",
86
+ "merge": "add",
87
+ "databaseType": "supabase"
88
+ },
89
+ {
90
+ "src": "drizzle/supabase/meta/_journal.json",
91
+ "dest": "drizzle/meta/_journal.json",
92
+ "merge": "add",
93
+ "databaseType": "supabase"
94
+ }
95
+ ],
96
+ "envVars": [
97
+ {
98
+ "key": "DATABASE_URL",
99
+ "description": "Database connection URL (libsql:// for Turso, postgresql:// for Supabase)",
100
+ "example": "libsql://your-db.turso.io",
101
+ "required": true
102
+ },
103
+ {
104
+ "key": "DATABASE_AUTH_TOKEN",
105
+ "description": "Database auth token (Turso only)",
106
+ "example": "eyJ...",
107
+ "required": false
108
+ }
109
+ ]
110
+ }
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { BalanceDisplay } from '../../../components/fedi/BalanceDisplay';
6
+ import { FediVersionBadge } from '../../../components/fedi/FediVersionBadge';
7
+
8
+ const DEMO_INSTALL_APP = {
9
+ id: 'com.create-fedi-app.demo',
10
+ title: 'Fedi Demo App',
11
+ url: 'https://example.com',
12
+ description: 'Sample mini app entry for the ecash-balance demo.',
13
+ };
14
+
15
+ export default function EcashDemoPage() {
16
+ const [howItWorksOpen, setHowItWorksOpen] = useState(false);
17
+
18
+ return (
19
+ <div className="min-h-dvh bg-[var(--color-bg)] font-[family-name:var(--font-body)] text-[var(--color-text)]">
20
+ <div
21
+ className="mx-auto w-full max-w-[390px] px-4 pt-6"
22
+ style={{ paddingBottom: 'max(5rem, env(safe-area-inset-bottom, 20px))' }}
23
+ >
24
+ <div className="mb-6 flex items-center justify-between gap-3">
25
+ <Link
26
+ href="/demo"
27
+ className="inline-block text-xs text-[var(--color-text-muted)] transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80"
28
+ >
29
+ ← back
30
+ </Link>
31
+ <FediVersionBadge />
32
+ </div>
33
+
34
+ <header className="mb-8 space-y-2">
35
+ <h1 className="font-[family-name:var(--font-display)] text-2xl font-bold leading-tight text-[var(--color-text)]">
36
+ Ecash &amp; Fedi API
37
+ </h1>
38
+ <p className="max-w-[75ch] text-sm leading-[1.65] text-[var(--color-text-muted)]">
39
+ Reads <code className="font-mono text-xs">window.fediInternal</code> for versioned mini
40
+ app discovery. Unlike WebLN and Nostr, this API is optional. Older Fedi builds may not
41
+ expose it.
42
+ </p>
43
+ </header>
44
+
45
+ <div className="space-y-8">
46
+ <section className="space-y-4">
47
+ <div className="max-w-[75ch] space-y-1.5">
48
+ <h2 className="font-[family-name:var(--font-display)] text-xl font-semibold leading-tight tracking-tight text-[var(--color-text)]">
49
+ Environment &amp; installed apps
50
+ </h2>
51
+ <p className="text-sm leading-[1.65] text-[var(--color-text-muted)]">
52
+ On fediInternal v2, lists installed mini app URLs (click to open) and can trigger
53
+ Fedi&apos;s native install sheet via <code className="font-mono text-xs">installMiniApp()</code>.
54
+ </p>
55
+ </div>
56
+ <BalanceDisplay installApp={DEMO_INSTALL_APP} />
57
+ </section>
58
+
59
+ <section className="space-y-3">
60
+ <button
61
+ type="button"
62
+ onClick={() => setHowItWorksOpen((open) => !open)}
63
+ className="flex w-full items-center justify-between rounded-lg px-4 py-3 text-left text-sm font-semibold transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80"
64
+ style={{
65
+ background: 'var(--color-surface-1)',
66
+ border: '1px solid var(--color-border)',
67
+ color: 'var(--color-text)',
68
+ borderRadius: 'var(--radius-lg)',
69
+ }}
70
+ aria-expanded={howItWorksOpen}
71
+ aria-controls="ecash-how-it-works"
72
+ >
73
+ How fediInternal works
74
+ <span aria-hidden>{howItWorksOpen ? '−' : '+'}</span>
75
+ </button>
76
+
77
+ {howItWorksOpen && (
78
+ <div
79
+ id="ecash-how-it-works"
80
+ className="space-y-3 rounded-lg px-4 py-3 text-sm leading-[1.65]"
81
+ style={{
82
+ background: 'var(--color-surface-1)',
83
+ border: '1px solid var(--color-border)',
84
+ color: 'var(--color-text-muted)',
85
+ borderRadius: 'var(--radius-lg)',
86
+ }}
87
+ >
88
+ <p>
89
+ <strong className="text-[var(--color-text)]">fediInternal</strong> is a versioned
90
+ bridge for mini app discovery inside Fedi. Version{' '}
91
+ <code className="font-mono text-xs">0</code> and{' '}
92
+ <code className="font-mono text-xs">1</code> only expose the version number;{' '}
93
+ <code className="font-mono text-xs">2</code> adds{' '}
94
+ <code className="font-mono text-xs">getInstalledMiniApps()</code> and{' '}
95
+ <code className="font-mono text-xs">installMiniApp()</code>.
96
+ </p>
97
+ <p>
98
+ Always check both presence and version before calling methods. Outside Fedi (or on
99
+ very old builds), <code className="font-mono text-xs">window.fediInternal</code> is{' '}
100
+ <code className="font-mono text-xs">undefined</code>. Show an &quot;Open in
101
+ Fedi&quot; prompt instead of failing silently.
102
+ </p>
103
+ <p>
104
+ Use <code className="font-mono text-xs">installMiniApp()</code> to suggest
105
+ companion apps to your users. Fedi shows a native confirmation sheet; your mini app
106
+ never writes to the user&apos;s home screen directly.
107
+ </p>
108
+ </div>
109
+ )}
110
+ </section>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ );
115
+ }