@pyric/cli 0.1.0-alpha.12 → 0.1.0-alpha.14

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 (230) hide show
  1. package/dist/cli/serve.js +5 -5
  2. package/dist/cli/serve.js.map +1 -1
  3. package/dist/serve/bundler.d.ts +15 -8
  4. package/dist/serve/bundler.d.ts.map +1 -1
  5. package/dist/serve/bundler.js +38 -37
  6. package/dist/serve/bundler.js.map +1 -1
  7. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  8. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  9. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  10. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  11. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  12. package/dist/serve/docs-ui/docs/api-reference/index.html +1 -1
  13. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  14. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
  16. package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
  17. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  19. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  20. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  23. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  24. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
  28. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
  29. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
  30. package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
  31. package/dist/serve/docs-ui/docs/index.json +149 -4
  32. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
  34. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
  35. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  36. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  37. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  38. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  39. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  40. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  41. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  42. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  50. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  51. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  57. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  58. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +38 -11
  60. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +26 -8
  61. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
  63. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
  64. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  68. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  69. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  71. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +1 -1
  72. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  80. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  81. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  99. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  100. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  101. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  102. package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
  103. package/dist/serve/docs-ui/docs/tutorial.md +701 -0
  104. package/dist/serve/docs-ui/llms.txt +2 -0
  105. package/dist/serve/entries/init.d.ts.map +1 -1
  106. package/dist/serve/entries/init.js +3 -0
  107. package/dist/serve/entries/init.js.map +1 -1
  108. package/dist/serve/entries/runtime.d.ts.map +1 -1
  109. package/dist/serve/entries/runtime.js +20 -5
  110. package/dist/serve/entries/runtime.js.map +1 -1
  111. package/dist/serve/entries/worker-runtime.d.ts +2 -2
  112. package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
  113. package/dist/serve/entries/worker-runtime.js +45 -10
  114. package/dist/serve/entries/worker-runtime.js.map +1 -1
  115. package/dist/serve/runtime/chip-config.d.ts +19 -0
  116. package/dist/serve/runtime/chip-config.d.ts.map +1 -0
  117. package/dist/serve/runtime/chip-config.js +19 -0
  118. package/dist/serve/runtime/chip-config.js.map +1 -0
  119. package/dist/serve/runtime/chip-install.d.ts +10 -0
  120. package/dist/serve/runtime/chip-install.d.ts.map +1 -0
  121. package/dist/serve/runtime/chip-install.js +15 -0
  122. package/dist/serve/runtime/chip-install.js.map +1 -0
  123. package/dist/serve/runtime/chip.d.ts +17 -0
  124. package/dist/serve/runtime/chip.d.ts.map +1 -0
  125. package/dist/serve/runtime/chip.js +257 -0
  126. package/dist/serve/runtime/chip.js.map +1 -0
  127. package/dist/serve/runtime/manifest.d.ts +22 -0
  128. package/dist/serve/runtime/manifest.d.ts.map +1 -0
  129. package/dist/serve/runtime/manifest.js +20 -0
  130. package/dist/serve/runtime/manifest.js.map +1 -0
  131. package/dist/serve/runtime/status.d.ts +49 -0
  132. package/dist/serve/runtime/status.d.ts.map +1 -0
  133. package/dist/serve/runtime/status.js +164 -0
  134. package/dist/serve/runtime/status.js.map +1 -0
  135. package/dist/serve/runtime/worker-connection.d.ts +4 -0
  136. package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
  137. package/dist/serve/runtime/worker-connection.js +11 -0
  138. package/dist/serve/runtime/worker-connection.js.map +1 -0
  139. package/dist/serve/runtime/worker-generation.d.ts +22 -0
  140. package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
  141. package/dist/serve/runtime/worker-generation.js +77 -0
  142. package/dist/serve/runtime/worker-generation.js.map +1 -0
  143. package/dist/serve/runtime/worker-replacement.d.ts +18 -0
  144. package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
  145. package/dist/serve/runtime/worker-replacement.js +29 -0
  146. package/dist/serve/runtime/worker-replacement.js.map +1 -0
  147. package/dist/serve/standalone-assets.d.ts +2 -2
  148. package/dist/serve/standalone-assets.d.ts.map +1 -1
  149. package/dist/serve/standalone-assets.js +1 -1
  150. package/dist/serve/standalone-assets.js.map +1 -1
  151. package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
  152. package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
  153. package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
  154. package/dist/serve/studio-ui/index.html +1 -1
  155. package/dist/serve/vite-ai-config.d.ts +27 -0
  156. package/dist/serve/vite-ai-config.d.ts.map +1 -0
  157. package/dist/serve/vite-ai-config.js +49 -0
  158. package/dist/serve/vite-ai-config.js.map +1 -0
  159. package/dist/serve/vite-plugin.d.ts +17 -36
  160. package/dist/serve/vite-plugin.d.ts.map +1 -1
  161. package/dist/serve/vite-plugin.js +38 -57
  162. package/dist/serve/vite-plugin.js.map +1 -1
  163. package/dist/serve/vite-rules-source.d.ts +8 -0
  164. package/dist/serve/vite-rules-source.d.ts.map +1 -0
  165. package/dist/serve/vite-rules-source.js +23 -0
  166. package/dist/serve/vite-rules-source.js.map +1 -0
  167. package/dist/serve/vite-worker-runtime.d.ts +20 -0
  168. package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
  169. package/dist/serve/vite-worker-runtime.js +29 -0
  170. package/dist/serve/vite-worker-runtime.js.map +1 -0
  171. package/dist/serve/worker/client/connection.d.ts +7 -2
  172. package/dist/serve/worker/client/connection.d.ts.map +1 -1
  173. package/dist/serve/worker/client/connection.js +13 -16
  174. package/dist/serve/worker/client/connection.js.map +1 -1
  175. package/dist/serve/worker/client/core.d.ts +7 -0
  176. package/dist/serve/worker/client/core.d.ts.map +1 -1
  177. package/dist/serve/worker/client/core.js +28 -0
  178. package/dist/serve/worker/client/core.js.map +1 -1
  179. package/dist/serve/worker/client/runtime-control.d.ts +8 -0
  180. package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
  181. package/dist/serve/worker/client/runtime-control.js +11 -0
  182. package/dist/serve/worker/client/runtime-control.js.map +1 -0
  183. package/dist/serve/worker/entry.js +44 -2
  184. package/dist/serve/worker/entry.js.map +1 -1
  185. package/dist/serve/worker/host/studio.js +1 -1
  186. package/dist/serve/worker/host/studio.js.map +1 -1
  187. package/dist/serve/worker/host-context.d.ts +1 -1
  188. package/dist/serve/worker/host-context.d.ts.map +1 -1
  189. package/dist/serve/worker/protocol.d.ts +15 -1
  190. package/dist/serve/worker/protocol.d.ts.map +1 -1
  191. package/dist/serve/worker/protocol.js.map +1 -1
  192. package/dist/serve/worker/retirement.d.ts +21 -0
  193. package/dist/serve/worker/retirement.d.ts.map +1 -0
  194. package/dist/serve/worker/retirement.js +97 -0
  195. package/dist/serve/worker/retirement.js.map +1 -0
  196. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  197. package/dist/serve/worker/serve-init.js +5 -6
  198. package/dist/serve/worker/serve-init.js.map +1 -1
  199. package/dist/vite.d.ts +1 -0
  200. package/dist/vite.d.ts.map +1 -1
  201. package/dist/vite.js.map +1 -1
  202. package/package.json +5 -4
  203. package/src/cli/serve.ts +5 -5
  204. package/src/serve/bundler.ts +49 -16
  205. package/src/serve/entries/init.ts +3 -0
  206. package/src/serve/entries/runtime.ts +23 -7
  207. package/src/serve/entries/worker-runtime.ts +69 -9
  208. package/src/serve/runtime/chip-config.ts +30 -0
  209. package/src/serve/runtime/chip-install.ts +27 -0
  210. package/src/serve/runtime/chip.ts +282 -0
  211. package/src/serve/runtime/manifest.ts +37 -0
  212. package/src/serve/runtime/status.ts +216 -0
  213. package/src/serve/runtime/worker-connection.ts +14 -0
  214. package/src/serve/runtime/worker-generation.ts +97 -0
  215. package/src/serve/runtime/worker-replacement.ts +42 -0
  216. package/src/serve/standalone-assets.ts +2 -2
  217. package/src/serve/vite-ai-config.ts +85 -0
  218. package/src/serve/vite-plugin.ts +61 -87
  219. package/src/serve/vite-rules-source.ts +28 -0
  220. package/src/serve/vite-worker-runtime.ts +58 -0
  221. package/src/serve/worker/client/connection.ts +29 -4
  222. package/src/serve/worker/client/core.ts +34 -0
  223. package/src/serve/worker/client/runtime-control.ts +22 -0
  224. package/src/serve/worker/entry.ts +46 -2
  225. package/src/serve/worker/host/studio.ts +1 -1
  226. package/src/serve/worker/host-context.ts +1 -1
  227. package/src/serve/worker/protocol.ts +13 -1
  228. package/src/serve/worker/retirement.ts +125 -0
  229. package/src/serve/worker/serve-init.ts +5 -6
  230. package/src/vite.ts +1 -0
@@ -0,0 +1,701 @@
1
+
2
+ # 12 years of Firebase experience distilled into one tool
3
+
4
+ Atwood's Law states that anything that can be written in JavaScript will be written in JavaScript. Atwood's Law has now come for Firebase.
5
+
6
+ I worked on Firebase for 12 years. I love how quickly it lets you build, but I also spent those years running into the same problem: developing against a cloud Firebase project.
7
+
8
+ Firebase provides the Emulator Suite, but it is Java-dependent, can be frustrating to connect to the SDKs, and leaves some production services without a local equivalent. I wanted an emulator that started with the app, required no separate runtime, and could not accidentally write to production. The browser is already one of the safest and most accessible sandboxes in the world, so I put the backend there.
9
+
10
+ ## Meet Pyric
11
+
12
+ ```bash
13
+ npm create pyric@latest my-chat -- --template chat
14
+ cd my-chat
15
+ npm install
16
+ npm run dev
17
+ ```
18
+
19
+ Pyric is a local mirror of Firebase. Open the Vite URL and the app runs Firebase code, but not against a cloud Firebase project. It runs against a backend in the browser. Choose a local user, send a message, then open Studio at `/__pyric/ui/` to see the data and requests.
20
+
21
+ A mirror is not a copy of Firebase's production infrastructure. Pyric implements the behavior the application can observe: a user signs in, a rule allows or denies a request, a snapshot changes, or a message arrives. It tests that supported behavior against Firebase and documents where the reflection ends.
22
+
23
+ We are going to build that chat app piece by piece. One conversation will show what Pyric adds to Authentication, Firestore, Security Rules, AI Logic, Storage, Realtime Database, Cloud Functions, and Cloud Messaging—and why I wanted each tool in the first place.
24
+
25
+ ## Start with `/improve-firebase`
26
+
27
+ Firebase projects rarely describe themselves in one place. The application imports one set of services, `firebase.json` points to another set of files, Security Rules describe the access model, and Functions add server behavior.
28
+
29
+ Before changing the app, ask Pyric to read those pieces together:
30
+
31
+ ```text
32
+ /improve-firebase
33
+ ```
34
+
35
+ The skill inspects the project without editing it. For our chat app, its first pass should find something like this:
36
+
37
+ ```text
38
+ Firebase services found:
39
+ ✓ Authentication
40
+ ✓ Firestore
41
+ ✓ Storage
42
+ ✓ Realtime Database
43
+ ✓ AI Logic
44
+ ✓ Cloud Messaging
45
+
46
+ Missing or unproven:
47
+ - local service configuration
48
+ - conversation ownership tests
49
+ - indexes required by the conversation queries
50
+ - a captured journey to verify before production
51
+ ```
52
+
53
+ It writes findings and plans under `plans/`. Review a plan, then let the skill carry it out explicitly:
54
+
55
+ ```text
56
+ /improve-firebase execute plans/001-local-chat.md
57
+ ```
58
+
59
+ This gives us a path through the application based on what is actually present. The first change is to put its Firebase calls behind Pyric during development.
60
+
61
+ ## Put the backend in the browser
62
+
63
+ Add the Vite plugin to an existing Firebase application:
64
+
65
+ ```bash
66
+ npm install --save-dev @pyric/cli
67
+ ```
68
+
69
+ Keep the Firebase imports in the application:
70
+
71
+ ```ts
72
+ import { getAI } from 'firebase/ai';
73
+ import { getAuth } from 'firebase/auth';
74
+ import { getDatabase } from 'firebase/database';
75
+ import { getFirestore } from 'firebase/firestore';
76
+ import { getStorage } from 'firebase/storage';
77
+
78
+ export const auth = getAuth(firebaseApp);
79
+ export const db = getFirestore(firebaseApp);
80
+ export const rtdb = getDatabase(firebaseApp);
81
+ export const storage = getStorage(firebaseApp);
82
+ export const ai = getAI(firebaseApp);
83
+ ```
84
+
85
+ Then add Pyric to Vite:
86
+
87
+ ```ts
88
+ // vite.config.ts
89
+ import { defineConfig } from 'vite';
90
+ import react from '@vitejs/plugin-react';
91
+ import { pyric } from '@pyric/cli/vite';
92
+
93
+ export default defineConfig({
94
+ plugins: [react(), pyric()],
95
+ });
96
+ ```
97
+
98
+ Run the app:
99
+
100
+ ```bash
101
+ npm run dev
102
+ ```
103
+
104
+ Under Vite, `firebase/firestore` and the other supported `firebase/*` imports resolve to Pyric. During a production build, they resolve to Firebase as usual. The application source does not need a local version and a production version.
105
+
106
+ The browser backend has two main pieces:
107
+
108
+ ```text
109
+ Tab A ─┐
110
+ ├── SharedWorker ── IndexedDB
111
+ Tab B ─┘
112
+ ```
113
+
114
+ A **SharedWorker** is a background browser process that multiple tabs can use together. **IndexedDB** is the database built into the browser. Pyric combines them so every tab on the same development origin shares local users and data. If the browser does not support SharedWorker, Pyric falls back to a sandbox contained in each tab.
115
+
116
+ Open the chat app twice. Create a Firestore document in the first tab and attach an `onSnapshot` listener in the second:
117
+
118
+ ```ts
119
+ onSnapshot(doc(db, 'conversations', 'welcome'), (snapshot) => {
120
+ console.log(snapshot.data());
121
+ });
122
+ ```
123
+
124
+ The second tab receives the update. There is no database server waiting on another port. For this application, the browser is the server.
125
+
126
+ Open Pyric Studio at `/__pyric/ui/`. The new document appears there too because Studio connects to the same browser backend. The app, its other tabs, Studio, and a connected coding agent all see the same local state.
127
+
128
+ ## Connect a coding agent with Pyric MCP
129
+
130
+ Enable the MCP bridge in Vite:
131
+
132
+ ```ts
133
+ pyric({ bridge: true })
134
+ ```
135
+
136
+ Pyric mounts an MCP server at `http://localhost:<vite-port>/__pyric/mcp`. Most MCP clients can use the port-discovering stdio configuration instead of a fixed URL:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "pyric": {
142
+ "command": "npx",
143
+ "args": ["--package", "@pyric/cli", "pyric", "mcp"]
144
+ }
145
+ }
146
+ }
147
+ ```
148
+
149
+ The MCP server handles Rules tools locally. For data tools, it forwards the agent's call over a WebSocket bridge to the SharedWorker that owns the browser sandbox. Keep the app open while the agent works, and do not start a second Pyric server beside Vite—that would create a second sandbox.
150
+
151
+ See [Connect an agent to the sandbox](./agent/set-up-your-agent.md) for Claude Code, Cursor, Codex, and generic MCP configuration.
152
+
153
+ ## Sign in without an OAuth provider
154
+
155
+ Google sign-in normally needs an OAuth provider and a cloud Firebase project. The chat app still uses the ordinary Firebase call:
156
+
157
+ ```ts
158
+ const provider = new GoogleAuthProvider();
159
+ const credential = await signInWithPopup(auth, provider);
160
+ console.log(credential.user.uid);
161
+ ```
162
+
163
+ In production, that opens Google's OAuth flow. Under Pyric, it opens a local account picker. Create Alice and Bob. The app receives normal Firebase users, and we can immediately test that Alice owns her conversation while Bob does not.
164
+
165
+ ## Give Security Rules the module system they need
166
+
167
+ Security Rules are some of the most important code in a Firebase application. They are also written in a language with no module system.
168
+
169
+ After enough projects, one Rules file becomes a collection of copied helpers, naming conventions, and comments asking everyone to keep three almost-identical functions in sync. Security code deserves a better foundation than copy and paste.
170
+
171
+ Pyric's `2+modules` format adds imports:
172
+
173
+ ```rules
174
+ rules_version = '2+modules';
175
+
176
+ import { isAuthenticated, isOwner } from 'auth';
177
+ import { hasRequired, hasOnly, validString } from 'validation';
178
+ import { immutableFields, onlyFieldsChanged, isServerTimestamp } from 'lifecycle';
179
+
180
+ service cloud.firestore {
181
+ match /databases/{database}/documents {
182
+ match /conversations/{conversationId} {
183
+ allow create: if isAuthenticated()
184
+ && isOwner(request.resource.data.ownerUid)
185
+ && hasRequired(['ownerUid', 'title', 'createdAt', 'updatedAt'])
186
+ && hasOnly(['ownerUid', 'title', 'createdAt', 'updatedAt'])
187
+ && validString('title', 1, 120)
188
+ && isServerTimestamp('createdAt')
189
+ && isServerTimestamp('updatedAt');
190
+
191
+ allow update: if isOwner(resource.data.ownerUid)
192
+ && immutableFields(['ownerUid', 'createdAt'])
193
+ && onlyFieldsChanged(['title', 'updatedAt'])
194
+ && isServerTimestamp('updatedAt');
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ Firebase never sees these imports. Resolve them before deployment:
201
+
202
+ ```bash
203
+ pyric firestore rules resolve firestore.modules.rules --out firestore.rules
204
+ ```
205
+
206
+ The resolver follows imports and their dependencies, renames private helpers so modules cannot collide, and produces an ordinary `rules_version = '2'` file. We get modules. Firebase gets the format it already understands.
207
+
208
+ Pyric loads `firestore.modules.rules` while Vite is running, so an edit takes effect without resolving the file first. `firestore.rules` remains the production artifact.
209
+
210
+ ## A Standard Library for Security Rules
211
+
212
+ Imports solve how to share a Rules function. They do not solve whether that function is safe.
213
+
214
+ I wanted a Standard Library for Security Rules: functions for the checks almost every application writes, followed by functions for the checks most applications avoid because they are difficult to get right.
215
+
216
+ The library begins with `auth`, `validation`, `lifecycle`, `membership`, `transitions`, and `counters`. Every module has executable allow-and-deny cases that run continuously against Pyric's Rules engine. The more difficult modules are also checked against Firebase rather than merely agreeing with Pyric's own implementation.
217
+
218
+ The chat rules already use the common end of that library:
219
+
220
+ ```rules
221
+ isOwner(request.resource.data.ownerUid)
222
+ hasRequired(['ownerUid', 'title', 'createdAt', 'updatedAt'])
223
+ validString('title', 1, 120)
224
+ immutableFields(['ownerUid', 'createdAt'])
225
+ isServerTimestamp('updatedAt')
226
+ ```
227
+
228
+ The `timing` module makes rate limiting possible in Security Rules. Suppose a conversation title may only change once every two seconds:
229
+
230
+ ```rules
231
+ import { cooldownElapsed } from 'timing';
232
+ import { isServerTimestamp } from 'lifecycle';
233
+
234
+ allow update: if isOwner(resource.data.ownerUid)
235
+ && onlyFieldsChanged(['title', 'updatedAt'])
236
+ && cooldownElapsed('updatedAt', 2)
237
+ && isServerTimestamp('updatedAt');
238
+ ```
239
+
240
+ `cooldownElapsed()` compares the previous trusted timestamp with `request.time`. It only works for updates because a new document has no previous value. Pair it with `isServerTimestamp()` so the client cannot forge the next timestamp. The comparison is strict: a write at the exact boundary is denied, while the first write after it is allowed.
241
+
242
+ The `geometry` module makes games possible. Geometry is what made it possible to build chess in Firestore Security Rules: the rule can read the stored piece, check its legal destination, and reject an impossible move before it reaches the database. The Standard Library distills that work into config-driven movement helpers:
243
+
244
+ ```rules
245
+ import { validSimpleMove, validJumpMove } from 'geometry';
246
+
247
+ function config() {
248
+ return get(/databases/$(database)/documents/gameConfig/checkers).data;
249
+ }
250
+
251
+ allow update: if ownsGame()
252
+ && moveIntegrity()
253
+ && (validSimpleMove(config())
254
+ || (validJumpMove(config()) && captureValid()));
255
+ ```
256
+
257
+ The configuration document records which starting and ending cells form a valid move. For a jump, it also records the cell that must be captured. The helper reads the moving piece from the stored board, not from a client claim, and uses the configuration to reject an impossible move. The surrounding checks still enforce ownership, board integrity, and the captured piece; geometry is one tested part of the complete rule.
258
+
259
+ Our chat app does not need checkers geometry. That is not the point. The same library spans “is this my document?” and “is this a legal jump over the correct board position?” It is accumulated Security Rules work that the next project should not have to rediscover.
260
+
261
+ Ask a connected agent to inspect the library rather than guess a helper name:
262
+
263
+ > List the Standard Library functions for ownership, document validation, and update timing. Show their signatures and tests before using them.
264
+
265
+ The agent uses `firestore_rules_stdlib_list` and `firestore_rules_stdlib_get`. Once our rules are assembled, `firestore_lint_rules` checks their syntax and Firebase limits. `firestore_simulate_rules` tests the actual boundary:
266
+
267
+ ```text
268
+ ALLOW create conversations/c1 as alice
269
+ DENY create conversations/c1 as bob
270
+ Reason: request.auth.uid does not match ownerUid
271
+ ```
272
+
273
+ Changing only the user makes this a useful test. We are not checking whether a request can succeed. We are checking who is allowed to make it.
274
+
275
+ ## See each request, not just its error
276
+
277
+ Create `conversations/c1` as Alice, switch to Bob, and try to read it. Studio's Traffic view puts both decisions together:
278
+
279
+ ```text
280
+ ALLOW alice create conversations/c1
281
+ DENY bob get conversations/c1
282
+ ```
283
+
284
+ Open the denial to see the request data and Rules result. This is the answer I always wanted instead of a bare `permission-denied`.
285
+
286
+ A coding agent sees the same backend:
287
+
288
+ > Inspect the local backend. Show the loaded rules, document counts, and recent denied requests. Do not change anything.
289
+
290
+ ```text
291
+ Firestore rules: loaded from firestore.modules.rules
292
+ Documents: conversations 1
293
+ Recent denial: bob tried to read conversations/c1
294
+ ```
295
+
296
+ Studio is the visual view. `sandbox_inspect` is the agent's view. Neither reads production data.
297
+
298
+ ## Generate indexes without breaking a query
299
+
300
+ The chat app lists Alice's conversations by their latest update:
301
+
302
+ ```ts
303
+ query(
304
+ collection(db, 'conversations'),
305
+ where('ownerUid', '==', uid),
306
+ orderBy('updatedAt', 'desc'),
307
+ limit(100),
308
+ );
309
+ ```
310
+
311
+ It also lists the messages in a conversation:
312
+
313
+ ```ts
314
+ query(
315
+ collection(db, 'conversations', conversationId, 'messages'),
316
+ where('ownerUid', '==', uid),
317
+ orderBy('createdAt', 'desc'),
318
+ limit(100),
319
+ );
320
+ ```
321
+
322
+ Firestore usually teaches you about a composite index by breaking the query. The application runs, the query fails, and the error gives you a link that fills out part of the Firebase Console. The alternative is to keep the query open in your editor while entering the same fields, directions, and scope by hand.
323
+
324
+ Neither is a good workflow. One requires a failure. The other requires describing the same query twice. I always wanted a query analyzer that could read the query and generate the index, so that is what Pyric has:
325
+
326
+ ```bash
327
+ pyric firestore indexes generate src --out /tmp/pychat-indexes.json
328
+ ```
329
+
330
+ For those two queries, it produces:
331
+
332
+ ```json
333
+ {
334
+ "indexes": [
335
+ {
336
+ "collectionGroup": "conversations",
337
+ "queryScope": "COLLECTION",
338
+ "fields": [
339
+ { "fieldPath": "ownerUid", "order": "ASCENDING" },
340
+ { "fieldPath": "updatedAt", "order": "DESCENDING" }
341
+ ]
342
+ },
343
+ {
344
+ "collectionGroup": "messages",
345
+ "queryScope": "COLLECTION",
346
+ "fields": [
347
+ { "fieldPath": "ownerUid", "order": "ASCENDING" },
348
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
349
+ ]
350
+ }
351
+ ],
352
+ "fieldOverrides": []
353
+ }
354
+ ```
355
+
356
+ The equality filter supplies `ownerUid`. The sort supplies the date field and its direction. Pyric derives both indexes from the application code; Firebase still creates them in production.
357
+
358
+ ## Answer AI Logic with a model on your machine
359
+
360
+ Firebase AI Logic is an amazing service. It gives a browser application a Firebase-shaped API for working with generative models. The chat app uses it without a Pyric-specific call:
361
+
362
+ ```ts
363
+ const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' });
364
+ const result = await model.generateContentStream({ contents });
365
+ ```
366
+
367
+ With no AI configuration, Pyric answers that call with a deterministic scripted response. This makes the first run and automated tests predictable without installing a model.
368
+
369
+ For development, I also wanted to keep the real model loop local. If Ollama is already running on my machine, the Firebase call should be able to use it. Start the app with both the server and model named:
370
+
371
+ ```bash
372
+ PYRIC_AI_MODEL=qwen3:4b \
373
+ PYRIC_AI_PROXY_UPSTREAM=http://localhost:11434/v1 \
374
+ npm run dev
375
+ ```
376
+
377
+ The **upstream** is the OpenAI-compatible server Pyric forwards to. The **model** is the model name sent to that server. Ollama's default upstream is already `http://localhost:11434/v1`, but writing both here makes the relationship visible.
378
+
379
+ The variables must prefix `npm run dev`. This does not pass them to Vite:
380
+
381
+ ```bash
382
+ PYRIC_AI_MODEL=qwen3:4b PYRIC_AI_PROXY_UPSTREAM=http://localhost:11434/v1 && npm run dev
383
+ ```
384
+
385
+ The `&&` ends the assignment command before Vite starts. For a persistent setup, put the same values in `.env.local` and restart Vite:
386
+
387
+ ```dotenv
388
+ PYRIC_AI_MODEL=qwen3:4b
389
+ PYRIC_AI_PROXY_UPSTREAM=http://localhost:11434/v1
390
+ ```
391
+
392
+ The Vite configuration stays `pyric()`. Pyric reads the environment, maps the Firebase model request to the selected local model, and proxies the browser request through Vite so Ollama needs no browser CORS configuration. The app still calls `firebase/ai`; only the destination changed.
393
+
394
+ ## Check attachments with local Storage
395
+
396
+ A message can include a file. The application uploads it with the Firebase Storage SDK:
397
+
398
+ ```ts
399
+ const path = `users/${uid}/conversations/${conversationId}/attachments/${attachmentId}/${file.name}`;
400
+ await uploadBytes(ref(storage, path), file, { contentType: file.type });
401
+ ```
402
+
403
+ Pyric's Storage mirror stores the bytes locally and evaluates `storage.rules`. The chat rule does more than compare the path UID. It reads the conversation from Firestore and confirms that the uploader owns it:
404
+
405
+ ```rules
406
+ allow write: if request.auth != null
407
+ && request.auth.uid == uid
408
+ && firestore.get(
409
+ /databases/(default)/documents/conversations/$(conversationId)
410
+ ).data.ownerUid == request.auth.uid
411
+ && request.resource.size <= 10 * 1024 * 1024;
412
+ ```
413
+
414
+ Upload as Alice, then try the same path as Bob. Studio shows the stored file for Alice and the denied request for Bob. The Storage mirror and Firestore mirror are not isolated toys; the Storage rule can consult the same local Firestore data.
415
+
416
+ ## Write Realtime Database Rules with TypeScript
417
+
418
+ Presence uses Realtime Database because its live tree is a natural fit for who is online:
419
+
420
+ ```ts
421
+ await set(ref(rtdb, `presence/${user.uid}`), {
422
+ state: 'online',
423
+ displayName: user.displayName,
424
+ at: serverTimestamp(),
425
+ });
426
+ ```
427
+
428
+ Open a second tab and watch Alice appear. The RTDB mirror sends the update through the same browser backend.
429
+
430
+ Realtime Database Rules are normally strings inside a JSON tree. They also cascade: a `.write` granted at one path grants every path below it, and a child cannot take that permission back. Pyric's constraints system lets us write the access rules and data schema in TypeScript before either becomes JSON.
431
+
432
+ Install Zod, then define the data once in `src/firebase/rtdb-schema.ts`:
433
+
434
+ ```bash
435
+ npm install zod
436
+ ```
437
+
438
+ ```ts
439
+ import { z } from 'zod';
440
+
441
+ export const PresenceRecordSchema = z.object({
442
+ state: z.enum(['online', 'offline']),
443
+ displayName: z.union([z.string(), z.literal(null)]),
444
+ at: z.number(),
445
+ });
446
+
447
+ export type PresenceRecord = z.infer<typeof PresenceRecordSchema>;
448
+
449
+ export const NotificationRequestSchema = z.object({
450
+ title: z.string(),
451
+ body: z.string(),
452
+ conversationId: z.string(),
453
+ createdAt: z.number(),
454
+ });
455
+
456
+ export type NotificationRequest = z.infer<typeof NotificationRequestSchema>;
457
+ ```
458
+
459
+ `z.infer` creates the application types. Pyric compiles the same schemas into RTDB `.validate` rules, so the types and backend validation cannot quietly drift apart.
460
+
461
+ Use those schemas in `database.rules.ts`:
462
+
463
+ ```ts
464
+ import {
465
+ authenticated,
466
+ defineRtdbRules,
467
+ deny,
468
+ pathOwnerOnly,
469
+ } from 'pyric/rules';
470
+ import {
471
+ NotificationRequestSchema,
472
+ PresenceRecordSchema,
473
+ } from './src/firebase/rtdb-schema';
474
+
475
+ export default defineRtdbRules({
476
+ paths: {
477
+ '/': { read: deny(), write: deny() },
478
+
479
+ '/presence': { read: authenticated() },
480
+ '/presence/$uid': {
481
+ write: pathOwnerOnly('$uid'),
482
+ schema: PresenceRecordSchema,
483
+ },
484
+
485
+ '/notify/$uid': {
486
+ read: pathOwnerOnly('$uid'),
487
+ write: pathOwnerOnly('$uid'),
488
+ children: {
489
+ '/$pushId': {
490
+ schema: NotificationRequestSchema,
491
+ },
492
+ },
493
+ },
494
+ },
495
+ });
496
+ ```
497
+
498
+ The root starts closed. `pathOwnerOnly('$uid')` combines authentication with the path check, so Alice can write `/presence/alice` but not `/presence/bob`. Under `/notify`, the owner grant cascades to each pushed notification while its schema checks every expected value.
499
+
500
+ Compile the constraints into the JSON that Pyric and Firebase load:
501
+
502
+ ```bash
503
+ pyric database rules generate
504
+ pyric database rules lint database.rules.json
505
+ ```
506
+
507
+ The TypeScript file is the source people edit. `database.rules.json` is the generated deployment artifact. The schema compiler currently supports objects composed from strings, numbers, booleans, enums, literals, unions, nested objects, and optional fields. Unsupported shapes fail generation instead of becoming weaker Rules.
508
+
509
+ Now test the four boundaries that matter:
510
+
511
+ ```text
512
+ ALLOW write /presence/alice as alice
513
+ DENY write /presence/alice signed out
514
+ DENY write /presence/alice as bob
515
+ DENY write /presence/alice as alice with state: "nearby"
516
+ ```
517
+
518
+ Use `rtdb_simulate_access` against the running sandbox for these checks. Pass the same constraints to `rtdbRules(...)` for in-process tests.
519
+
520
+ The current chat template still authors `database.rules.json` directly. Moving it to this constraints source is a follow-up; the rule behavior shown here is the target rather than a file the template already contains.
521
+
522
+ Pyric does not currently mirror RTDB `onDisconnect`. The template writes the offline state explicitly during local development and uses the real `onDisconnect` behavior in production. This is one of the places where the mirror has a visible edge rather than pretending to be complete.
523
+
524
+ ## Run Cloud Functions against the browser backend
525
+
526
+ The notification path needs server code. When Alice comes online, an RTDB `onValueCreated` Cloud Function stamps her Firestore profile. There is an odd consequence to running that Function locally: server code needs to reach a backend living in the browser.
527
+
528
+ The chat app runs a Function when `/presence/{uid}` is created:
529
+
530
+ ```js
531
+ export const onPresenceOnline = onValueCreated('/presence/{uid}', async (event) => {
532
+ const profile = getFirestore().collection('users').doc(event.params.uid);
533
+ const snapshot = await profile.get();
534
+ const now = FieldValue.serverTimestamp();
535
+
536
+ await profile.set(
537
+ snapshot.data()?.firstSeenAt
538
+ ? { lastSeenAt: now }
539
+ : { firstSeenAt: now, lastSeenAt: now },
540
+ { merge: true },
541
+ );
542
+ });
543
+ ```
544
+
545
+ During development, Pyric starts the supported RTDB `onValueCreated` Functions in a Node child process. It maps `firebase-admin/*` imports to Pyric's Admin SDK mirror and connects that process to the browser sandbox through a WebSocket bridge:
546
+
547
+ ```text
548
+ Node Function
549
+
550
+ │ WebSocket bridge
551
+
552
+ SharedWorker ── IndexedDB
553
+
554
+
555
+ browser tabs and Studio
556
+ ```
557
+
558
+ Yes, the server connects to the browser. It sounds backward until you remember that the browser sandbox is the source of truth for this development session.
559
+
560
+ Create Alice's presence record. The Function reads her local Firestore profile through the Admin SDK mirror, writes `firstSeenAt`, and the open tabs receive that update. Admin writes bypass client Security Rules, matching the trusted role of a deployed Function.
561
+
562
+ Pyric currently runs RTDB `onValueCreated` triggers locally and reports other trigger types as unsupported. That boundary keeps a working local path from turning into a broader promise.
563
+
564
+ ## Deliver a notification without production Messaging
565
+
566
+ Cloud Messaging has always been difficult to develop locally because its normal path keeps reaching into production. The browser needs a registration token. The backend needs credentials. Delivery leaves the machine and eventually finds its way back to the browser.
567
+
568
+ Pyric keeps that loop local. The app still uses the Firebase Messaging SDK:
569
+
570
+ ```ts
571
+ const token = await getToken(getMessaging(firebaseApp), {
572
+ serviceWorkerRegistration,
573
+ });
574
+
575
+ onMessage(getMessaging(firebaseApp), (message) => {
576
+ showToast(message.notification?.title ?? 'New message');
577
+ });
578
+ ```
579
+
580
+ Pyric creates a local token. The app saves it to Alice's Firestore profile. When the assistant reply is ready, it writes a notification request to `/notify/alice/{pushId}`. A local Function reads the token with the Admin SDK mirror and calls `getMessaging().send()`.
581
+
582
+ A **service worker** is a browser script that can receive a notification while the page is hidden. Pyric sends a message to `onMessage` while the page is visible and to the service worker's `onBackgroundMessage` handler while it is hidden. Test both paths: keep the chat visible for the first reply, then hide it before sending the second.
583
+
584
+ The Firebase-shaped path is intact—client token, backend send, foreground or background delivery—but the notification never passes through Firebase Cloud Messaging. Keep at least one app page open because closing every page ends the local backend. Production delivery after every page is closed remains Firebase behavior.
585
+
586
+ ## See when the browser backend needs attention
587
+
588
+ A SharedWorker can outlive a Vite update. The runtime chip warns when new application code is connected to an older Pyric worker.
589
+
590
+ Collapsed, it shows two indicators:
591
+
592
+ - sandbox errors;
593
+ - an available worker update.
594
+
595
+ Open it to copy an error, update the worker, or launch Studio. Updating finishes accepted work, saves captured state, and reloads the connected tabs. The chip is development-only.
596
+
597
+ ## Check the conversation before production
598
+
599
+ To save a reusable local backend, enable disk persistence in Vite:
600
+
601
+ ```ts
602
+ pyric({ persist: true })
603
+ ```
604
+
605
+ Once it contains useful users and conversations, promote that state to a fixture. Use the Vite port printed by `npm run dev`:
606
+
607
+ ```bash
608
+ pyric snapshot --port 5173 --out fixtures/chat-ready.json
609
+ ```
610
+
611
+ `pyric snapshot` saves the Firestore documents and Auth users. Passwords are redacted by default. Use that fixture as the starting state later:
612
+
613
+ ```ts
614
+ pyric({ seed: 'fixtures/chat-ready.json' })
615
+ ```
616
+
617
+ Seeds apply to an empty sandbox. Reset the existing sandbox in Studio before expecting the fixture to replace earlier browser state.
618
+
619
+ A snapshot preserves the starting point. A captured session preserves what the app did next.
620
+
621
+ Exercise the complete conversation:
622
+
623
+ 1. Sign in as Alice.
624
+ 2. Create a conversation and send a message.
625
+ 3. Switch to Bob and confirm that the conversation read is denied.
626
+ 4. Return to Alice and receive the AI reply.
627
+ 5. Upload an attachment.
628
+ 6. Write Alice's presence.
629
+ 7. Let the Functions update her profile and send a notification.
630
+
631
+ Pyric records those Firestore and RTDB operations, their users, and their Rules results in `.pyric/last-session.json`. Replay that journey against the current Rules:
632
+
633
+ ```bash
634
+ pyric verify --rules firestore=firestore.rules
635
+ ```
636
+
637
+ A changed ownership rule might report:
638
+
639
+ ```text
640
+ now-denied: create conversations/c1 as alice
641
+ ```
642
+
643
+ `now-denied` means a request that worked in the capture is rejected now. `now-allowed` means a previous denial is now allowed. The snapshot gives the test its state; the session gives it the behavior to check.
644
+
645
+ For an important Firestore Rules change, compare the same cases with Firebase's Rules Test API:
646
+
647
+ ```bash
648
+ pyric verify \
649
+ --service firestore \
650
+ --engine both \
651
+ --project my-app \
652
+ --rules firestore=firestore.rules
653
+ ```
654
+
655
+ This comparison requires Firebase credentials, but it does not deploy Rules or change production data. Hosted comparison currently covers Firestore, not RTDB or Storage.
656
+
657
+ Pyric is not a complete implementation of Firebase. That is why these trust boundaries matter. The goal is to learn as much as possible locally, show what was actually tested, and make the remaining production step deliberate.
658
+
659
+ ## Prepare for a Firebase Cloud Project
660
+
661
+ Prepare the generated Rules and indexes, then run the application's checks:
662
+
663
+ ```bash
664
+ npm run rules:resolve
665
+ pyric database rules generate
666
+ pyric firestore indexes generate src --out firestore.indexes.json
667
+ npm test
668
+ npm run typecheck
669
+ ```
670
+
671
+ Build with the normal production mode:
672
+
673
+ ```bash
674
+ npm --prefix functions install
675
+ npm run build
676
+ ```
677
+
678
+ The production build resolves `firebase/*` and `firebase-admin/*` to the real Firebase packages. Pyric Studio, the runtime chip, local users, local data, and the development mirrors are absent.
679
+
680
+ Pyric tests the supported behavior your application can observe. It does not reproduce a cloud project's IAM, quotas, latency, regional configuration, billing, or every Firebase API. Before deployment, use a separate Firebase project to check:
681
+
682
+ - real OAuth redirects and authorized domains;
683
+ - deployed Firestore, RTDB, and Storage Rules;
684
+ - composite index creation;
685
+ - Cloud Functions triggers and Admin SDK permissions;
686
+ - Cloud Messaging tokens and background delivery;
687
+ - the hosted AI model and its limits.
688
+
689
+ Be diligent here. Local confidence should make cloud testing smaller and more focused, not replace it. Deploy with the Firebase CLI when those checks pass:
690
+
691
+ ```bash
692
+ npx firebase-tools deploy
693
+ ```
694
+
695
+ We started with Firebase calls in a chat app and followed them into a backend running in the browser. That backend signed in users, synchronized data between tabs, evaluated modular Security Rules, served files, tracked presence, answered AI Logic through Ollama, connected to server code through the Admin SDK bridge, and delivered a local notification.
696
+
697
+ Along the way, we used the tools I wanted during 12 years of Firebase development: a Security Rules module system, a heavily tested Standard Library, an index analyzer, a backend we could inspect, and a way to check the complete journey before production.
698
+
699
+ The application stayed Firebase. Development stayed local. That is Pyric.
700
+
701
+ Continue with [How the swap works](./get-started/how-the-swap-works.md), [The Rules Standard Library](./secure/rules-standard-library.md), or [Ship to production](./ship/ship-to-production.md).