lytx 0.3.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 (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
package/.env.example ADDED
@@ -0,0 +1,37 @@
1
+ # ── Lytx Kit – Environment Variables ──
2
+ # Copy this file to .env and fill in your values:
3
+ # cp .env.example .env
4
+
5
+ # Domain that appears in emails and auth callbacks
6
+ LYTX_DOMAIN=localhost:5173
7
+
8
+ # Auth (required)
9
+ BETTER_AUTH_SECRET=
10
+ BETTER_AUTH_URL=http://localhost:5173
11
+ ENCRYPTION_KEY=
12
+
13
+ # Email sender address (used for verification, invites, etc.)
14
+ EMAIL_FROM=noreply@yourdomain.com
15
+
16
+ # Auth providers (optional – fill in to enable)
17
+ GITHUB_CLIENT_ID=
18
+ GITHUB_CLIENT_SECRET=
19
+ GOOGLE_CLIENT_ID=
20
+ GOOGLE_CLIENT_SECRET=
21
+
22
+ # Email via Resend (optional – required for verification/invite emails)
23
+ RESEND_API_KEY=
24
+
25
+ # AI features (optional)
26
+ AI_API_KEY=
27
+ AI_BASE_URL=
28
+ AI_MODEL=
29
+ AI_DAILY_TOKEN_LIMIT=
30
+
31
+ # Report builder feature toggles
32
+ REPORT_BUILDER=false
33
+ ASK_AI=true
34
+
35
+ # Misc
36
+ SEED_DATA_SECRET=
37
+ ENVIRONMENT=development
package/README.md ADDED
@@ -0,0 +1,486 @@
1
+ # Lytx Kit – Core
2
+
3
+ Open-source web analytics platform built on [RedwoodSDK](https://rwsdk.com) (rwsdk) and Cloudflare Workers. Ship a full analytics dashboard — event ingestion, dashboards, team management, auth — inside your own Redwood app.
4
+
5
+ ## OSS contract
6
+
7
+ The supported public API surface for `lytx` is documented in `core/docs/oss-contract.md`.
8
+
9
+ - Contract doc: [`docs/oss-contract.md`](./docs/oss-contract.md)
10
+ - Self-host quickstart: [`docs/self-host-quickstart.md`](./docs/self-host-quickstart.md)
11
+ - Semver/release policy: [`docs/release-policy.md`](./docs/release-policy.md)
12
+ - Upgrade/migration guide: [`docs/migration-guide.md`](./docs/migration-guide.md)
13
+ - Read this first before relying on any non-root or deep import path.
14
+
15
+ ## How it works
16
+
17
+ `lytx` exposes a canonical app factory, `createLytxApp`, from the package root. Use it to bootstrap a full worker without importing internals. For advanced composition, root exports also include route, page, middleware, and Durable Object building blocks.
18
+
19
+ An experimental pre-wired worker entrypoint also exists at `lytx/worker`; this entrypoint is intentionally not part of the stable API contract.
20
+
21
+ Think of it like a parts catalog: pull in the full analytics stack, or cherry-pick just the event ingestion API and build your own UI.
22
+
23
+ ## Prerequisites
24
+
25
+ - [Bun](https://bun.sh) (runtime)
26
+ - A Redwood SDK (rwsdk) project — `npx rwsdk@latest new my-app`
27
+ - Cloudflare account (D1, KV, Durable Objects, Queues)
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ # from your rwsdk project root
33
+ bun add lytx
34
+ ```
35
+
36
+ > Until this is published to npm, add it as a workspace dependency or link it locally.
37
+
38
+ ## Quick start — app factory (recommended)
39
+
40
+ Use the root app factory to bootstrap the full analytics stack with one import:
41
+
42
+ ```tsx
43
+ // src/worker.tsx
44
+ import type { ExportedHandler } from "cloudflare:workers";
45
+ import { createLytxApp, SyncDurableObject, SiteDurableObject } from "lytx";
46
+
47
+ const app = createLytxApp({
48
+ db: {
49
+ dbAdapter: "sqlite",
50
+ eventStore: "durable_objects",
51
+ },
52
+ auth: {
53
+ socialProviders: {
54
+ google: true,
55
+ github: false,
56
+ },
57
+ },
58
+ });
59
+
60
+ export { SyncDurableObject, SiteDurableObject };
61
+
62
+ export default app satisfies ExportedHandler<Env>;
63
+ ```
64
+
65
+ `createLytxApp` supports:
66
+
67
+ - `features.dashboard`, `features.events`, `features.auth`, `features.ai`, `features.tagScript`
68
+ - `db.dbAdapter` (`"sqlite" | "postgres" | "singlestore" | "analytics_engine"`)
69
+ - `db.eventStore` (`db.dbAdapter` values + `"durable_objects"`; defaults to `"durable_objects"`)
70
+ - `useQueueIngestion` (`true`/`false`)
71
+ - `includeLegacyTagRoutes` (`true` by default for `/lytx.js` and `/trackWebEvent` compatibility)
72
+ - `trackingRoutePrefix` (prefix all tracking routes, e.g. `/collect`)
73
+ - `tagRoutes.scriptPath` + `tagRoutes.eventPath` (custom v2 route paths)
74
+ - `auth.emailPasswordEnabled`, `auth.requireEmailVerification`, `auth.socialProviders.google`, `auth.socialProviders.github`
75
+ - `features.reportBuilderEnabled` + `features.askAiEnabled`
76
+ - `names.*` (typed resource binding names for D1/KV/Queue/DO)
77
+ - `domains.app` + `domains.tracking` (typed host/domain values)
78
+ - `startupValidation.*` + `env.*` (startup env requirement checks with field-level errors)
79
+ - `env.EMAIL_FROM` (optional factory override for outgoing email sender)
80
+
81
+ For deployment scripts, use `resolveLytxResourceNames(...)` from `lytx/resource-names` to derive deterministic Cloudflare resource names with optional stage-based prefix/suffix strategy.
82
+
83
+ ## Quick start — manual composition (advanced)
84
+
85
+ This drops the entire Lytx analytics platform into your Redwood app. Copy-paste into your `src/worker.tsx` and adjust as needed.
86
+
87
+ ```tsx
88
+ // src/worker.tsx
89
+ import { defineApp, type RequestInfo } from "rwsdk/worker";
90
+ import { route, render, prefix, layout } from "rwsdk/router";
91
+ import type { ExportedHandler } from "cloudflare:workers";
92
+ import { IS_DEV } from "rwsdk/constants";
93
+
94
+ import {
95
+ // Document shell
96
+ Document,
97
+
98
+ // Public pages
99
+ Signup,
100
+ Login,
101
+ VerifyEmail,
102
+
103
+ // Authenticated app pages
104
+ AppLayout,
105
+ DashboardPage,
106
+ EventsPage,
107
+ ExplorePage,
108
+ SettingsPage,
109
+ NewSiteSetup,
110
+ DashboardWorkspaceLayout,
111
+ ReportBuilderWorkspace,
112
+ CustomReportBuilderPage,
113
+
114
+ // API routes
115
+ eventsApi,
116
+ seedApi,
117
+ team_dashboard_endpoints,
118
+ world_countries,
119
+ getCurrentVisitorsRoute,
120
+ getDashboardDataRoute,
121
+ siteEventsSqlRoute,
122
+ siteEventsSchemaRoute,
123
+ aiChatRoute,
124
+ aiConfigRoute,
125
+ aiTagSuggestRoute,
126
+ resendVerificationEmailRoute,
127
+ userApiRoutes,
128
+ eventLabelsApi,
129
+ reportsApi,
130
+ legacyContainerRoute,
131
+ newSiteSetup,
132
+ lytxTag,
133
+ trackWebEvent,
134
+ handleQueueMessage,
135
+
136
+ // Middleware
137
+ authMiddleware,
138
+ sessionMiddleware,
139
+
140
+ // Auth
141
+ auth,
142
+
143
+ // Route guards
144
+ checkIfTeamSetupSites,
145
+ onlyAllowGetPost,
146
+
147
+ // Durable Objects (re-export so Cloudflare can find them)
148
+ SyncDurableObject,
149
+ SiteDurableObject,
150
+
151
+ // Types
152
+ type AppContext,
153
+ type DBAdapter,
154
+ } from "lytx";
155
+
156
+ export { SyncDurableObject, SiteDurableObject };
157
+
158
+ type AppRequestInfo = RequestInfo<any, AppContext>;
159
+
160
+ const dbAdapter: DBAdapter = "sqlite";
161
+
162
+ const app = defineApp<AppRequestInfo>([
163
+ ({ request }) => {
164
+ if (IS_DEV) console.log(request.method, request.url);
165
+ },
166
+
167
+ // ── Tag & event ingestion (unauthenticated) ──
168
+ legacyContainerRoute,
169
+ lytxTag(dbAdapter),
170
+ trackWebEvent(dbAdapter, "/trackWebEvent", { useQueue: true }),
171
+ eventsApi,
172
+ seedApi,
173
+
174
+ // ── Auth API ──
175
+ route("/api/auth/*", (r) => authMiddleware(r)),
176
+ resendVerificationEmailRoute,
177
+ userApiRoutes,
178
+
179
+ // ── Rendered pages ──
180
+ render<AppRequestInfo>(Document, [
181
+ route("/", [onlyAllowGetPost, ({ request }) => Response.redirect(new URL("/login", request.url).toString(), 308)]),
182
+ route("/signup", [onlyAllowGetPost, () => <Signup />]),
183
+ route("/login", [onlyAllowGetPost, () => <Login />]),
184
+ route("/verify-email", [
185
+ onlyAllowGetPost,
186
+ async ({ request }) => {
187
+ const url = new URL(request.url);
188
+ const token = url.searchParams.get("token") || "";
189
+ if (!token) {
190
+ return <VerifyEmail status={{ type: "error", message: "Missing token." }} />;
191
+ }
192
+ try {
193
+ await auth.api.verifyEmail({ query: { token } });
194
+ return <VerifyEmail status={{ type: "success", message: "Email verified." }} />;
195
+ } catch {
196
+ return <VerifyEmail status={{ type: "error", message: "Verification failed." }} />;
197
+ }
198
+ },
199
+ ]),
200
+
201
+ // ── Authenticated app shell ──
202
+ layout(AppLayout, [
203
+ sessionMiddleware,
204
+
205
+ // Authenticated API routes
206
+ prefix("/api", [
207
+ world_countries,
208
+ getDashboardDataRoute,
209
+ getCurrentVisitorsRoute,
210
+ aiConfigRoute,
211
+ aiChatRoute,
212
+ aiTagSuggestRoute,
213
+ siteEventsSqlRoute,
214
+ siteEventsSchemaRoute,
215
+ eventLabelsApi,
216
+ reportsApi,
217
+ newSiteSetup(),
218
+ team_dashboard_endpoints,
219
+ ]),
220
+
221
+ onlyAllowGetPost,
222
+
223
+ // Dashboard pages
224
+ route("/dashboard", [
225
+ checkIfTeamSetupSites,
226
+ () => <DashboardPage activeReportBuilderItemId="create-report" />,
227
+ ]),
228
+ layout(DashboardWorkspaceLayout, [
229
+ route("/dashboard/reports/create-report", [
230
+ checkIfTeamSetupSites,
231
+ () => <ReportBuilderWorkspace activeReportBuilderItemId="create-report" />,
232
+ ]),
233
+ route("/dashboard/reports/custom/new", [
234
+ checkIfTeamSetupSites,
235
+ ({ request }) => {
236
+ const template = new URL(request.url).searchParams.get("template");
237
+ return <CustomReportBuilderPage initialTemplate={template} />;
238
+ },
239
+ ]),
240
+ // ... add more report routes as needed
241
+ ]),
242
+ route("/dashboard/events", [checkIfTeamSetupSites, () => <EventsPage />]),
243
+ route("/dashboard/settings", [() => <SettingsPage />]),
244
+ route("/dashboard/explore", [checkIfTeamSetupSites, () => <ExplorePage />]),
245
+ route("/dashboard/new-site", [() => <NewSiteSetup />]),
246
+ ]),
247
+ ]),
248
+ ]);
249
+
250
+ export default {
251
+ fetch: app.fetch,
252
+ queue: handleQueueMessage,
253
+ } satisfies ExportedHandler<Env>;
254
+ ```
255
+
256
+ ## Consumer starter template
257
+
258
+ For a copy/paste starter workspace (worker + vite + `alchemy.run.ts`) that uses public root exports, see `demo/README.md`.
259
+
260
+ ## Minimal setup — event ingestion only
261
+
262
+ If you only need the tracking pixel and event API (no dashboard UI):
263
+
264
+ ```tsx
265
+ // src/worker.tsx
266
+ import { defineApp, type RequestInfo } from "rwsdk/worker";
267
+ import { route } from "rwsdk/router";
268
+ import type { ExportedHandler } from "cloudflare:workers";
269
+ import {
270
+ lytxTag,
271
+ trackWebEvent,
272
+ eventsApi,
273
+ handleQueueMessage,
274
+ authMiddleware,
275
+ type AppContext,
276
+ } from "lytx";
277
+
278
+ export { SiteDurableObject } from "lytx";
279
+
280
+ type AppRequestInfo = RequestInfo<any, AppContext>;
281
+
282
+ const app = defineApp<AppRequestInfo>([
283
+ lytxTag("sqlite"),
284
+ trackWebEvent("sqlite", "/trackWebEvent", { useQueue: true }),
285
+ eventsApi,
286
+ route("/api/auth/*", (r) => authMiddleware(r)),
287
+ ]);
288
+
289
+ export default {
290
+ fetch: app.fetch,
291
+ queue: handleQueueMessage,
292
+ } satisfies ExportedHandler<Env>;
293
+ ```
294
+
295
+ ## Cloudflare bindings
296
+
297
+ Your `wrangler.jsonc` (or `alchemy.run.ts`) needs these bindings for the full stack:
298
+
299
+ | Binding | Type | Purpose |
300
+ |---|---|---|
301
+ | `lytx_core_db` | D1 Database | Primary data store (users, teams, sites, events) |
302
+ | `LYTX_EVENTS` | KV Namespace | Event storage / caching |
303
+ | `lytx_config` | KV Namespace | Configuration store |
304
+ | `lytx_sessions` | KV Namespace | Session storage |
305
+ | `SITE_EVENTS_QUEUE` | Queue | Async event ingestion |
306
+ | `SITE_DURABLE_OBJECT` | Durable Object | Per-site event aggregation |
307
+
308
+ ### Resource naming strategy
309
+
310
+ Resource binding keys in worker code stay fixed (`LYTX_EVENTS`, `lytx_config`, etc.), but physical Cloudflare resource names can be configured deterministically in `alchemy.run.ts` via `resolveLytxResourceNames` (`lytx/resource-names`).
311
+
312
+ Supported naming env vars:
313
+
314
+ ```env
315
+ # Optional global strategy
316
+ LYTX_RESOURCE_PREFIX=
317
+ LYTX_RESOURCE_SUFFIX=
318
+ # one of: prefix | suffix | none
319
+ LYTX_RESOURCE_STAGE_POSITION=none
320
+
321
+ # Optional per-resource overrides
322
+ LYTX_WORKER_NAME=
323
+ LYTX_DURABLE_HOST_WORKER_NAME=
324
+ LYTX_DURABLE_OBJECT_NAMESPACE_NAME=
325
+ LYTX_D1_DATABASE_NAME=
326
+ LYTX_KV_EVENTS_NAME=
327
+ LYTX_KV_CONFIG_NAME=
328
+ LYTX_KV_SESSIONS_NAME=
329
+ LYTX_QUEUE_NAME=
330
+ ```
331
+
332
+ This keeps naming deterministic across deploys and avoids accidental resource drift between stages.
333
+
334
+ ### Domain and route prefix strategy
335
+
336
+ Use these env vars in `alchemy.run.ts` to configure app/tracking domains without editing source:
337
+
338
+ ```env
339
+ # Optional custom worker domain
340
+ LYTX_APP_DOMAIN=analytics.example.com
341
+
342
+ # Optional tracking domain used in LYTX_DOMAIN binding
343
+ LYTX_TRACKING_DOMAIN=collect.example.com
344
+ ```
345
+
346
+ Use `createLytxApp({ tagRoutes: { pathPrefix: "/collect" } })` to prefix tracking script and ingestion endpoints.
347
+
348
+ ### Environment variables
349
+
350
+ Add these to your `.env` (local) or worker secrets (production):
351
+
352
+ ```env
353
+ # Required
354
+ BETTER_AUTH_SECRET=<random-secret>
355
+ BETTER_AUTH_URL=http://localhost:5173
356
+ ENCRYPTION_KEY=<random-secret>
357
+
358
+ # Auth providers (optional — enable the ones you want)
359
+ GITHUB_CLIENT_ID=...
360
+ GITHUB_CLIENT_SECRET=...
361
+ GOOGLE_CLIENT_ID=...
362
+ GOOGLE_CLIENT_SECRET=...
363
+
364
+ # Email (required for verification/invite emails)
365
+ EMAIL_FROM=noreply@yourdomain.com
366
+ RESEND_API_KEY=...
367
+
368
+ # AI features (optional)
369
+ AI_API_KEY=...
370
+ AI_BASE_URL=...
371
+ AI_MODEL=...
372
+ AI_DAILY_TOKEN_LIMIT=
373
+
374
+ # Report builder toggle (optional)
375
+ # Set to `true` to enable report routes and UI
376
+ REPORT_BUILDER=false
377
+ # Set to `false` to hide Ask AI while keeping report builder enabled
378
+ ASK_AI=true
379
+
380
+ # Modular feature toggles (optional)
381
+ LYTX_FEATURE_DASHBOARD=true
382
+ LYTX_FEATURE_EVENTS=true
383
+ LYTX_FEATURE_AUTH=true
384
+ LYTX_FEATURE_AI=true
385
+ LYTX_FEATURE_TAG_SCRIPT=true
386
+
387
+ # Misc
388
+ LYTX_DOMAIN=localhost:5173
389
+ ENVIRONMENT=development
390
+ SEED_DATA_SECRET=<random-secret>
391
+ ```
392
+
393
+ If `EMAIL_FROM` is missing (or left as the placeholder `noreply@example.com`), email send attempts fail with a clear runtime error explaining how to configure it.
394
+
395
+ On a fresh install, the first successful signup becomes the initial admin and creates the default team. For scripted/bootstrap environments, you can use:
396
+
397
+ ```bash
398
+ cd core
399
+ bun run cli/bootstrap-admin.ts --email admin@example.com --password "StrongPassword123"
400
+ ```
401
+
402
+ Use `--remote` to apply bootstrap changes directly to Cloudflare D1 via Wrangler. This requires Wrangler authentication (`wrangler login` or a valid Cloudflare API token) and access to the target database.
403
+
404
+ ## Database setup
405
+
406
+ Generate and apply D1 migrations:
407
+
408
+ ```bash
409
+ bunx drizzle-kit generate --config=db/d1/drizzle.config.ts
410
+ wrangler d1 migrations apply lytx-core-db --local
411
+ ```
412
+
413
+ Seed dev data:
414
+
415
+ ```bash
416
+ bun run cli/seed-data.ts --team-id 1 --site-id 1 --durable-only --events 50 --seed-secret "$SEED_DATA_SECRET"
417
+ ```
418
+
419
+ ## What's included
420
+
421
+ ### App Factory
422
+
423
+ | Export | Description |
424
+ |---|---|
425
+ | `createLytxApp` | Canonical factory that returns a worker handler (`fetch` + `queue`) with configurable tag routes and feature toggles |
426
+
427
+ ### API Routes
428
+
429
+ | Export | Path | Description |
430
+ |---|---|---|
431
+ | `lytxTag` | `/lytx.js` | JavaScript tracking tag |
432
+ | `trackWebEvent` | `/trackWebEvent` | Event ingestion endpoint |
433
+ | `eventsApi` | `/api/events/*` | Event CRUD |
434
+ | `getDashboardDataRoute` | `/api/dashboard-data` | Dashboard aggregation |
435
+ | `getCurrentVisitorsRoute` | `/api/current-visitors` | Real-time visitor count |
436
+ | `siteEventsSqlRoute` | `/api/sql` | Raw SQL query interface |
437
+ | `team_dashboard_endpoints` | `/api/team/*` | Team management |
438
+ | `eventLabelsApi` | `/api/event-labels/*` | Event label CRUD |
439
+ | `reportsApi` | `/api/reports/*` | Custom reports |
440
+ | `aiChatRoute` | `/api/ai/chat` | AI data assistant |
441
+ | `authMiddleware` | `/api/auth/*` | better-auth handler |
442
+
443
+ ### Pages & Components
444
+
445
+ | Export | Description |
446
+ |---|---|
447
+ | `DashboardPage` | Main analytics dashboard with charts, maps, tables |
448
+ | `EventsPage` | Event explorer / raw event viewer |
449
+ | `ExplorePage` | SQL explorer with Monaco editor |
450
+ | `SettingsPage` | Team settings, API keys, site tag install |
451
+ | `Signup`, `Login`, `VerifyEmail` | Auth pages |
452
+ | `AppLayout` | Authenticated app shell with nav |
453
+ | `Document` | HTML document wrapper |
454
+
455
+ ### Middleware
456
+
457
+ | Export | Description |
458
+ |---|---|
459
+ | `authMiddleware` | Handles `/api/auth/*` (better-auth) |
460
+ | `sessionMiddleware` | Loads user session + team context into `AppContext` |
461
+ | `onlyAllowGetPost` | Rejects non-GET/POST requests |
462
+ | `checkIfTeamSetupSites` | Redirects to setup if team has no sites |
463
+
464
+ ### Durable Objects
465
+
466
+ | Export | Description |
467
+ |---|---|
468
+ | `SiteDurableObject` | Per-site event storage and aggregation |
469
+ | `SyncDurableObject` | Session synchronization |
470
+
471
+ > You **must** re-export Durable Objects from your worker entry point so Cloudflare can instantiate them.
472
+
473
+ ## Customization
474
+
475
+ Since you control `defineApp`, you can:
476
+
477
+ - **Drop routes** you don't need (remove the AI routes, the seed API, etc.)
478
+ - **Add your own routes** alongside Lytx routes
479
+ - **Replace pages** with your own React components while keeping the API routes
480
+ - **Mount under a prefix** — wrap Lytx routes in `prefix("/analytics", [...])`
481
+ - **Swap the DB adapter** — pass `"postgres"` instead of `"sqlite"` to tag routes
482
+ - **Add middleware** — insert your own auth/rate-limiting before or after `sessionMiddleware`
483
+
484
+ ## License
485
+
486
+ MIT
package/alchemy.run.ts ADDED
@@ -0,0 +1,155 @@
1
+ import type { SiteDurableObject } from "./db/durable/siteDurableObject";
2
+ import alchemy from "alchemy";
3
+ import {
4
+ D1Database,
5
+ KVNamespace,
6
+ DurableObjectNamespace,
7
+ Redwood,
8
+ Queue,
9
+ Worker,
10
+ } from "alchemy/cloudflare";
11
+ import {
12
+ resolveLytxResourceNames,
13
+ type LytxResourceStagePosition,
14
+ } from "./src/config/resourceNames";
15
+
16
+ const alchemyAppName = process.env.LYTX_APP_NAME ?? "lytx";
17
+ const app = await alchemy(alchemyAppName);
18
+ if (app.local && app.stage !== "dev") {
19
+ throw new Error(`Refusing local run on non-dev stage: ${app.stage}`);
20
+ }
21
+
22
+ const adoptMode = false;
23
+
24
+ const stagePositionRaw = process.env.LYTX_RESOURCE_STAGE_POSITION;
25
+ const stagePosition: LytxResourceStagePosition =
26
+ stagePositionRaw === "prefix" || stagePositionRaw === "suffix" || stagePositionRaw === "none"
27
+ ? stagePositionRaw
28
+ : "none";
29
+
30
+ const appDomain = process.env.LYTX_APP_DOMAIN?.trim();
31
+ const trackingDomain = process.env.LYTX_TRACKING_DOMAIN?.trim();
32
+
33
+ const resourceNames = resolveLytxResourceNames({
34
+ stage: app.stage,
35
+ prefix: process.env.LYTX_RESOURCE_PREFIX,
36
+ suffix: process.env.LYTX_RESOURCE_SUFFIX,
37
+ stagePosition,
38
+ overrides: {
39
+ workerName: process.env.LYTX_WORKER_NAME,
40
+ durableHostWorkerName: process.env.LYTX_DURABLE_HOST_WORKER_NAME,
41
+ durableObjectNamespaceName: process.env.LYTX_DURABLE_OBJECT_NAMESPACE_NAME,
42
+ d1DatabaseName: process.env.LYTX_D1_DATABASE_NAME,
43
+ eventsKvNamespaceName: process.env.LYTX_KV_EVENTS_NAME,
44
+ configKvNamespaceName: process.env.LYTX_KV_CONFIG_NAME,
45
+ sessionsKvNamespaceName: process.env.LYTX_KV_SESSIONS_NAME,
46
+ eventsQueueName: process.env.LYTX_QUEUE_NAME,
47
+ },
48
+ });
49
+
50
+ const siteDurableObject = DurableObjectNamespace<SiteDurableObject>(resourceNames.durableObjectNamespaceName, {
51
+ className: "SiteDurableObject",
52
+ sqlite: true,
53
+ });
54
+
55
+ const lytxKv = await KVNamespace(resourceNames.eventsKvNamespaceName, {
56
+ adopt: adoptMode,
57
+ delete: false,
58
+ });
59
+
60
+ const lytx_config = await KVNamespace(resourceNames.configKvNamespaceName, {
61
+ adopt: adoptMode,
62
+ delete: false,
63
+ });
64
+
65
+ const siteEventsQueue = await Queue(resourceNames.eventsQueueName, {
66
+ name: resourceNames.eventsQueueName,
67
+ adopt: adoptMode,
68
+ delete: false,
69
+ });
70
+
71
+ const lytx_sessions = await KVNamespace(resourceNames.sessionsKvNamespaceName, {
72
+ adopt: adoptMode,
73
+ delete: false,
74
+ });
75
+
76
+ const lytxCoreDb = await D1Database(resourceNames.d1DatabaseName, {
77
+ name: resourceNames.d1DatabaseName,
78
+ migrationsDir: "./db/d1/migrations",
79
+ adopt: adoptMode,
80
+ delete: false,
81
+ });
82
+
83
+ const localDurableHost = app.local
84
+ ? await Worker(resourceNames.durableHostWorkerName, {
85
+ entrypoint: "./endpoints/site_do_worker.ts",
86
+ bindings: {
87
+ SITE_DURABLE_OBJECT: siteDurableObject,
88
+ lytx_core_db: lytxCoreDb,
89
+ ENVIRONMENT: process.env.ENVIRONMENT ?? "development",
90
+ },
91
+ })
92
+ : undefined;
93
+
94
+ export const worker = await Redwood(resourceNames.workerName, {
95
+ adopt: false,
96
+ url: false,
97
+ noBundle: false,
98
+ ...(appDomain
99
+ ? {
100
+ domains: [
101
+ {
102
+ adopt: adoptMode,
103
+ domainName: appDomain,
104
+ },
105
+ ],
106
+ }
107
+ : {}),
108
+ wrangler: {
109
+ main: "src/worker.tsx",
110
+ transform: (spec) => ({
111
+ ...spec,
112
+ compatibility_flags: ["nodejs_compat"],
113
+ }),
114
+ },
115
+ eventSources: [
116
+ {
117
+ queue: siteEventsQueue,
118
+ settings: {
119
+ batchSize: 100,
120
+ maxConcurrency: 4,
121
+ maxRetries: 3,
122
+ maxWaitTimeMs: 20000,
123
+ retryDelay: 30,
124
+ },
125
+ },
126
+ ],
127
+ bindings: {
128
+ SITE_DURABLE_OBJECT: siteDurableObject,
129
+ LYTX_EVENTS: lytxKv,
130
+ lytx_config: lytx_config,
131
+ lytx_sessions: lytx_sessions,
132
+ lytx_core_db: lytxCoreDb,
133
+ SITE_EVENTS_QUEUE: siteEventsQueue,
134
+ LYTX_DOMAIN: trackingDomain || appDomain || process.env.LYTX_DOMAIN || "localhost:5173",
135
+ EMAIL_FROM: process.env.EMAIL_FROM || "noreply@example.com",
136
+ BETTER_AUTH_SECRET: alchemy.secret(process.env.BETTER_AUTH_SECRET),
137
+ GITHUB_CLIENT_SECRET: alchemy.secret(process.env.GITHUB_CLIENT_SECRET),
138
+ GOOGLE_CLIENT_SECRET: alchemy.secret(process.env.GOOGLE_CLIENT_SECRET),
139
+ RESEND_API_KEY: alchemy.secret(process.env.RESEND_API_KEY),
140
+ ENCRYPTION_KEY: alchemy.secret(process.env.ENCRYPTION_KEY),
141
+ AI_API_KEY: alchemy.secret(process.env.AI_API_KEY),
142
+ SEED_DATA_SECRET: alchemy.secret(process.env.SEED_DATA_SECRET),
143
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
144
+ GITHUB_CLIENT_ID: alchemy.secret(process.env.GITHUB_CLIENT_ID),
145
+ GOOGLE_CLIENT_ID: alchemy.secret(process.env.GOOGLE_CLIENT_ID),
146
+ ENVIRONMENT: process.env.ENVIRONMENT ?? "development",
147
+ REPORT_BUILDER: process.env.REPORT_BUILDER ?? "false",
148
+ ASK_AI: process.env.ASK_AI ?? "true",
149
+ AI_BASE_URL: process.env.AI_BASE_URL ?? "",
150
+ AI_MODEL: process.env.AI_MODEL ?? "",
151
+ AI_DAILY_TOKEN_LIMIT: process.env.AI_DAILY_TOKEN_LIMIT ?? "",
152
+ },
153
+ });
154
+
155
+ await app.finalize();