@digimuza/saas-core 0.0.2

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 (474) hide show
  1. package/CLAUDE.md +1165 -0
  2. package/LICENSE +21 -0
  3. package/README.md +234 -0
  4. package/dist/api-keys/index.d.ts +9 -0
  5. package/dist/api-keys/index.d.ts.map +1 -0
  6. package/dist/api-keys/index.js +37 -0
  7. package/dist/api-keys/index.js.map +1 -0
  8. package/dist/api-keys/keys.d.ts +55 -0
  9. package/dist/api-keys/keys.d.ts.map +1 -0
  10. package/dist/api-keys/keys.js +92 -0
  11. package/dist/api-keys/keys.js.map +1 -0
  12. package/dist/api-keys/service.d.ts +111 -0
  13. package/dist/api-keys/service.d.ts.map +1 -0
  14. package/dist/api-keys/service.js +210 -0
  15. package/dist/api-keys/service.js.map +1 -0
  16. package/dist/audit/index.d.ts +5 -0
  17. package/dist/audit/index.d.ts.map +1 -0
  18. package/dist/audit/index.js +44 -0
  19. package/dist/audit/index.js.map +1 -0
  20. package/dist/audit/service.d.ts +88 -0
  21. package/dist/audit/service.d.ts.map +1 -0
  22. package/dist/audit/service.js +128 -0
  23. package/dist/audit/service.js.map +1 -0
  24. package/dist/auth/client.d.ts +5 -0
  25. package/dist/auth/client.d.ts.map +1 -0
  26. package/dist/auth/client.js +26 -0
  27. package/dist/auth/client.js.map +1 -0
  28. package/dist/auth/cookies.d.ts +51 -0
  29. package/dist/auth/cookies.d.ts.map +1 -0
  30. package/dist/auth/cookies.js +58 -0
  31. package/dist/auth/cookies.js.map +1 -0
  32. package/dist/auth/index.d.ts +14 -0
  33. package/dist/auth/index.d.ts.map +1 -0
  34. package/dist/auth/index.js +46 -0
  35. package/dist/auth/index.js.map +1 -0
  36. package/dist/auth/service.d.ts +67 -0
  37. package/dist/auth/service.d.ts.map +1 -0
  38. package/dist/auth/service.js +275 -0
  39. package/dist/auth/service.js.map +1 -0
  40. package/dist/auth/types.d.ts +56 -0
  41. package/dist/auth/types.d.ts.map +1 -0
  42. package/dist/auth/types.js +2 -0
  43. package/dist/auth/types.js.map +1 -0
  44. package/dist/authorization/index.d.ts +4 -0
  45. package/dist/authorization/index.d.ts.map +1 -0
  46. package/dist/authorization/index.js +24 -0
  47. package/dist/authorization/index.js.map +1 -0
  48. package/dist/authorization/service.d.ts +92 -0
  49. package/dist/authorization/service.d.ts.map +1 -0
  50. package/dist/authorization/service.js +254 -0
  51. package/dist/authorization/service.js.map +1 -0
  52. package/dist/billing/index.d.ts +16 -0
  53. package/dist/billing/index.d.ts.map +1 -0
  54. package/dist/billing/index.js +47 -0
  55. package/dist/billing/index.js.map +1 -0
  56. package/dist/billing/provider.d.ts +114 -0
  57. package/dist/billing/provider.d.ts.map +1 -0
  58. package/dist/billing/provider.js +2 -0
  59. package/dist/billing/provider.js.map +1 -0
  60. package/dist/billing/revolut.d.ts +68 -0
  61. package/dist/billing/revolut.d.ts.map +1 -0
  62. package/dist/billing/revolut.js +181 -0
  63. package/dist/billing/revolut.js.map +1 -0
  64. package/dist/billing/service.d.ts +124 -0
  65. package/dist/billing/service.d.ts.map +1 -0
  66. package/dist/billing/service.js +470 -0
  67. package/dist/billing/service.js.map +1 -0
  68. package/dist/cli/commands/dev.d.ts +43 -0
  69. package/dist/cli/commands/dev.d.ts.map +1 -0
  70. package/dist/cli/commands/dev.js +212 -0
  71. package/dist/cli/commands/dev.js.map +1 -0
  72. package/dist/cli/commands/project.d.ts +12 -0
  73. package/dist/cli/commands/project.d.ts.map +1 -0
  74. package/dist/cli/commands/project.js +49 -0
  75. package/dist/cli/commands/project.js.map +1 -0
  76. package/dist/cli/docker.d.ts +50 -0
  77. package/dist/cli/docker.d.ts.map +1 -0
  78. package/dist/cli/docker.js +84 -0
  79. package/dist/cli/docker.js.map +1 -0
  80. package/dist/cli/index.d.ts +6 -0
  81. package/dist/cli/index.d.ts.map +1 -0
  82. package/dist/cli/index.js +198 -0
  83. package/dist/cli/index.js.map +1 -0
  84. package/dist/cli/output.d.ts +30 -0
  85. package/dist/cli/output.d.ts.map +1 -0
  86. package/dist/cli/output.js +66 -0
  87. package/dist/cli/output.js.map +1 -0
  88. package/dist/cli/templates.d.ts +52 -0
  89. package/dist/cli/templates.d.ts.map +1 -0
  90. package/dist/cli/templates.js +225 -0
  91. package/dist/cli/templates.js.map +1 -0
  92. package/dist/config/client.d.ts +60 -0
  93. package/dist/config/client.d.ts.map +1 -0
  94. package/dist/config/client.js +94 -0
  95. package/dist/config/client.js.map +1 -0
  96. package/dist/config/config-loader.d.ts +2 -0
  97. package/dist/config/config-loader.d.ts.map +1 -0
  98. package/dist/config/config-loader.js +2 -0
  99. package/dist/config/config-loader.js.map +1 -0
  100. package/dist/config/env.d.ts +24 -0
  101. package/dist/config/env.d.ts.map +1 -0
  102. package/dist/config/env.js +40 -0
  103. package/dist/config/env.js.map +1 -0
  104. package/dist/config/errors.d.ts +17 -0
  105. package/dist/config/errors.d.ts.map +1 -0
  106. package/dist/config/errors.js +26 -0
  107. package/dist/config/errors.js.map +1 -0
  108. package/dist/config/index.d.ts +72 -0
  109. package/dist/config/index.d.ts.map +1 -0
  110. package/dist/config/index.js +106 -0
  111. package/dist/config/index.js.map +1 -0
  112. package/dist/config/runtime.d.ts +15 -0
  113. package/dist/config/runtime.d.ts.map +1 -0
  114. package/dist/config/runtime.js +22 -0
  115. package/dist/config/runtime.js.map +1 -0
  116. package/dist/config/sections.d.ts +226 -0
  117. package/dist/config/sections.d.ts.map +1 -0
  118. package/dist/config/sections.js +141 -0
  119. package/dist/config/sections.js.map +1 -0
  120. package/dist/db/client.d.ts +43 -0
  121. package/dist/db/client.d.ts.map +1 -0
  122. package/dist/db/client.js +77 -0
  123. package/dist/db/client.js.map +1 -0
  124. package/dist/db/errors.d.ts +39 -0
  125. package/dist/db/errors.d.ts.map +1 -0
  126. package/dist/db/errors.js +67 -0
  127. package/dist/db/errors.js.map +1 -0
  128. package/dist/db/index.d.ts +10 -0
  129. package/dist/db/index.d.ts.map +1 -0
  130. package/dist/db/index.js +10 -0
  131. package/dist/db/index.js.map +1 -0
  132. package/dist/db/migrate.d.ts +21 -0
  133. package/dist/db/migrate.d.ts.map +1 -0
  134. package/dist/db/migrate.js +28 -0
  135. package/dist/db/migrate.js.map +1 -0
  136. package/dist/db/schema/_shared.d.ts +30 -0
  137. package/dist/db/schema/_shared.d.ts.map +1 -0
  138. package/dist/db/schema/_shared.js +36 -0
  139. package/dist/db/schema/_shared.js.map +1 -0
  140. package/dist/db/schema/apikeys.d.ts +263 -0
  141. package/dist/db/schema/apikeys.d.ts.map +1 -0
  142. package/dist/db/schema/apikeys.js +70 -0
  143. package/dist/db/schema/apikeys.js.map +1 -0
  144. package/dist/db/schema/authorization.d.ts +465 -0
  145. package/dist/db/schema/authorization.d.ts.map +1 -0
  146. package/dist/db/schema/authorization.js +134 -0
  147. package/dist/db/schema/authorization.js.map +1 -0
  148. package/dist/db/schema/billing.d.ts +1100 -0
  149. package/dist/db/schema/billing.d.ts.map +1 -0
  150. package/dist/db/schema/billing.js +223 -0
  151. package/dist/db/schema/billing.js.map +1 -0
  152. package/dist/db/schema/events.d.ts +161 -0
  153. package/dist/db/schema/events.d.ts.map +1 -0
  154. package/dist/db/schema/events.js +66 -0
  155. package/dist/db/schema/events.js.map +1 -0
  156. package/dist/db/schema/features.d.ts +243 -0
  157. package/dist/db/schema/features.d.ts.map +1 -0
  158. package/dist/db/schema/features.js +87 -0
  159. package/dist/db/schema/features.js.map +1 -0
  160. package/dist/db/schema/files.d.ts +253 -0
  161. package/dist/db/schema/files.d.ts.map +1 -0
  162. package/dist/db/schema/files.js +74 -0
  163. package/dist/db/schema/files.js.map +1 -0
  164. package/dist/db/schema/index.d.ts +38 -0
  165. package/dist/db/schema/index.d.ts.map +1 -0
  166. package/dist/db/schema/index.js +38 -0
  167. package/dist/db/schema/index.js.map +1 -0
  168. package/dist/db/schema/invitations.d.ts +214 -0
  169. package/dist/db/schema/invitations.d.ts.map +1 -0
  170. package/dist/db/schema/invitations.js +80 -0
  171. package/dist/db/schema/invitations.js.map +1 -0
  172. package/dist/db/schema/messaging.d.ts +442 -0
  173. package/dist/db/schema/messaging.d.ts.map +1 -0
  174. package/dist/db/schema/messaging.js +120 -0
  175. package/dist/db/schema/messaging.js.map +1 -0
  176. package/dist/db/schema/users.d.ts +246 -0
  177. package/dist/db/schema/users.d.ts.map +1 -0
  178. package/dist/db/schema/users.js +60 -0
  179. package/dist/db/schema/users.js.map +1 -0
  180. package/dist/db/schema/webhooks.d.ts +693 -0
  181. package/dist/db/schema/webhooks.d.ts.map +1 -0
  182. package/dist/db/schema/webhooks.js +147 -0
  183. package/dist/db/schema/webhooks.js.map +1 -0
  184. package/dist/db/schema/workspaces.d.ts +196 -0
  185. package/dist/db/schema/workspaces.d.ts.map +1 -0
  186. package/dist/db/schema/workspaces.js +57 -0
  187. package/dist/db/schema/workspaces.js.map +1 -0
  188. package/dist/db/types.d.ts +18 -0
  189. package/dist/db/types.d.ts.map +1 -0
  190. package/dist/db/types.js +2 -0
  191. package/dist/db/types.js.map +1 -0
  192. package/dist/email/adapters.d.ts +29 -0
  193. package/dist/email/adapters.d.ts.map +1 -0
  194. package/dist/email/adapters.js +60 -0
  195. package/dist/email/adapters.js.map +1 -0
  196. package/dist/email/index.d.ts +20 -0
  197. package/dist/email/index.d.ts.map +1 -0
  198. package/dist/email/index.js +79 -0
  199. package/dist/email/index.js.map +1 -0
  200. package/dist/email/provider.d.ts +47 -0
  201. package/dist/email/provider.d.ts.map +1 -0
  202. package/dist/email/provider.js +7 -0
  203. package/dist/email/provider.js.map +1 -0
  204. package/dist/email/resend.d.ts +22 -0
  205. package/dist/email/resend.d.ts.map +1 -0
  206. package/dist/email/resend.js +51 -0
  207. package/dist/email/resend.js.map +1 -0
  208. package/dist/email/service.d.ts +55 -0
  209. package/dist/email/service.d.ts.map +1 -0
  210. package/dist/email/service.js +87 -0
  211. package/dist/email/service.js.map +1 -0
  212. package/dist/email/templates.d.ts +81 -0
  213. package/dist/email/templates.d.ts.map +1 -0
  214. package/dist/email/templates.js +84 -0
  215. package/dist/email/templates.js.map +1 -0
  216. package/dist/entitlements/index.d.ts +10 -0
  217. package/dist/entitlements/index.d.ts.map +1 -0
  218. package/dist/entitlements/index.js +28 -0
  219. package/dist/entitlements/index.js.map +1 -0
  220. package/dist/entitlements/service.d.ts +83 -0
  221. package/dist/entitlements/service.d.ts.map +1 -0
  222. package/dist/entitlements/service.js +158 -0
  223. package/dist/entitlements/service.js.map +1 -0
  224. package/dist/errors.d.ts +85 -0
  225. package/dist/errors.d.ts.map +1 -0
  226. package/dist/errors.js +95 -0
  227. package/dist/errors.js.map +1 -0
  228. package/dist/events/core-events.d.ts +120 -0
  229. package/dist/events/core-events.d.ts.map +1 -0
  230. package/dist/events/core-events.js +109 -0
  231. package/dist/events/core-events.js.map +1 -0
  232. package/dist/events/index.d.ts +15 -0
  233. package/dist/events/index.d.ts.map +1 -0
  234. package/dist/events/index.js +58 -0
  235. package/dist/events/index.js.map +1 -0
  236. package/dist/events/publisher.d.ts +46 -0
  237. package/dist/events/publisher.d.ts.map +1 -0
  238. package/dist/events/publisher.js +54 -0
  239. package/dist/events/publisher.js.map +1 -0
  240. package/dist/events/registry.d.ts +87 -0
  241. package/dist/events/registry.d.ts.map +1 -0
  242. package/dist/events/registry.js +34 -0
  243. package/dist/events/registry.js.map +1 -0
  244. package/dist/events/service.d.ts +78 -0
  245. package/dist/events/service.d.ts.map +1 -0
  246. package/dist/events/service.js +182 -0
  247. package/dist/events/service.js.map +1 -0
  248. package/dist/features/bucket.d.ts +22 -0
  249. package/dist/features/bucket.d.ts.map +1 -0
  250. package/dist/features/bucket.js +42 -0
  251. package/dist/features/bucket.js.map +1 -0
  252. package/dist/features/index.d.ts +15 -0
  253. package/dist/features/index.d.ts.map +1 -0
  254. package/dist/features/index.js +78 -0
  255. package/dist/features/index.js.map +1 -0
  256. package/dist/features/registry.d.ts +51 -0
  257. package/dist/features/registry.d.ts.map +1 -0
  258. package/dist/features/registry.js +52 -0
  259. package/dist/features/registry.js.map +1 -0
  260. package/dist/features/service.d.ts +78 -0
  261. package/dist/features/service.d.ts.map +1 -0
  262. package/dist/features/service.js +207 -0
  263. package/dist/features/service.js.map +1 -0
  264. package/dist/files/index.d.ts +6 -0
  265. package/dist/files/index.d.ts.map +1 -0
  266. package/dist/files/index.js +64 -0
  267. package/dist/files/index.js.map +1 -0
  268. package/dist/files/service.d.ts +92 -0
  269. package/dist/files/service.d.ts.map +1 -0
  270. package/dist/files/service.js +303 -0
  271. package/dist/files/service.js.map +1 -0
  272. package/dist/index.d.ts +33 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +33 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/jobs/backend.d.ts +23 -0
  277. package/dist/jobs/backend.d.ts.map +1 -0
  278. package/dist/jobs/backend.js +23 -0
  279. package/dist/jobs/backend.js.map +1 -0
  280. package/dist/jobs/index.d.ts +29 -0
  281. package/dist/jobs/index.d.ts.map +1 -0
  282. package/dist/jobs/index.js +114 -0
  283. package/dist/jobs/index.js.map +1 -0
  284. package/dist/jobs/service.d.ts +67 -0
  285. package/dist/jobs/service.d.ts.map +1 -0
  286. package/dist/jobs/service.js +139 -0
  287. package/dist/jobs/service.js.map +1 -0
  288. package/dist/jobs/types.d.ts +78 -0
  289. package/dist/jobs/types.d.ts.map +1 -0
  290. package/dist/jobs/types.js +4 -0
  291. package/dist/jobs/types.js.map +1 -0
  292. package/dist/logger/default.d.ts +7 -0
  293. package/dist/logger/default.d.ts.map +1 -0
  294. package/dist/logger/default.js +28 -0
  295. package/dist/logger/default.js.map +1 -0
  296. package/dist/logger/index.d.ts +24 -0
  297. package/dist/logger/index.d.ts.map +1 -0
  298. package/dist/logger/index.js +90 -0
  299. package/dist/logger/index.js.map +1 -0
  300. package/dist/logger/redact.d.ts +40 -0
  301. package/dist/logger/redact.d.ts.map +1 -0
  302. package/dist/logger/redact.js +178 -0
  303. package/dist/logger/redact.js.map +1 -0
  304. package/dist/logger/service.d.ts +14 -0
  305. package/dist/logger/service.d.ts.map +1 -0
  306. package/dist/logger/service.js +56 -0
  307. package/dist/logger/service.js.map +1 -0
  308. package/dist/logger/sinks.d.ts +37 -0
  309. package/dist/logger/sinks.d.ts.map +1 -0
  310. package/dist/logger/sinks.js +135 -0
  311. package/dist/logger/sinks.js.map +1 -0
  312. package/dist/logger/types.d.ts +47 -0
  313. package/dist/logger/types.d.ts.map +1 -0
  314. package/dist/logger/types.js +8 -0
  315. package/dist/logger/types.js.map +1 -0
  316. package/dist/notifications/index.d.ts +8 -0
  317. package/dist/notifications/index.d.ts.map +1 -0
  318. package/dist/notifications/index.js +72 -0
  319. package/dist/notifications/index.js.map +1 -0
  320. package/dist/notifications/service.d.ts +79 -0
  321. package/dist/notifications/service.d.ts.map +1 -0
  322. package/dist/notifications/service.js +164 -0
  323. package/dist/notifications/service.js.map +1 -0
  324. package/dist/realtime/channels.d.ts +37 -0
  325. package/dist/realtime/channels.d.ts.map +1 -0
  326. package/dist/realtime/channels.js +58 -0
  327. package/dist/realtime/channels.js.map +1 -0
  328. package/dist/realtime/index.d.ts +19 -0
  329. package/dist/realtime/index.d.ts.map +1 -0
  330. package/dist/realtime/index.js +118 -0
  331. package/dist/realtime/index.js.map +1 -0
  332. package/dist/realtime/postgres.d.ts +48 -0
  333. package/dist/realtime/postgres.d.ts.map +1 -0
  334. package/dist/realtime/postgres.js +151 -0
  335. package/dist/realtime/postgres.js.map +1 -0
  336. package/dist/realtime/service.d.ts +75 -0
  337. package/dist/realtime/service.d.ts.map +1 -0
  338. package/dist/realtime/service.js +103 -0
  339. package/dist/realtime/service.js.map +1 -0
  340. package/dist/realtime/sse.d.ts +55 -0
  341. package/dist/realtime/sse.d.ts.map +1 -0
  342. package/dist/realtime/sse.js +123 -0
  343. package/dist/realtime/sse.js.map +1 -0
  344. package/dist/realtime/transport.d.ts +52 -0
  345. package/dist/realtime/transport.d.ts.map +1 -0
  346. package/dist/realtime/transport.js +2 -0
  347. package/dist/realtime/transport.js.map +1 -0
  348. package/dist/storage/index.d.ts +18 -0
  349. package/dist/storage/index.d.ts.map +1 -0
  350. package/dist/storage/index.js +89 -0
  351. package/dist/storage/index.js.map +1 -0
  352. package/dist/storage/keys.d.ts +50 -0
  353. package/dist/storage/keys.d.ts.map +1 -0
  354. package/dist/storage/keys.js +120 -0
  355. package/dist/storage/keys.js.map +1 -0
  356. package/dist/storage/memory.d.ts +32 -0
  357. package/dist/storage/memory.d.ts.map +1 -0
  358. package/dist/storage/memory.js +84 -0
  359. package/dist/storage/memory.js.map +1 -0
  360. package/dist/storage/provider.d.ts +93 -0
  361. package/dist/storage/provider.d.ts.map +1 -0
  362. package/dist/storage/provider.js +2 -0
  363. package/dist/storage/provider.js.map +1 -0
  364. package/dist/storage/s3.d.ts +34 -0
  365. package/dist/storage/s3.d.ts.map +1 -0
  366. package/dist/storage/s3.js +175 -0
  367. package/dist/storage/s3.js.map +1 -0
  368. package/dist/storage/service.d.ts +86 -0
  369. package/dist/storage/service.d.ts.map +1 -0
  370. package/dist/storage/service.js +107 -0
  371. package/dist/storage/service.js.map +1 -0
  372. package/dist/storage/sigv4.d.ts +58 -0
  373. package/dist/storage/sigv4.d.ts.map +1 -0
  374. package/dist/storage/sigv4.js +114 -0
  375. package/dist/storage/sigv4.js.map +1 -0
  376. package/dist/streams/client.d.ts +91 -0
  377. package/dist/streams/client.d.ts.map +1 -0
  378. package/dist/streams/client.js +128 -0
  379. package/dist/streams/client.js.map +1 -0
  380. package/dist/streams/events.d.ts +68 -0
  381. package/dist/streams/events.d.ts.map +1 -0
  382. package/dist/streams/events.js +81 -0
  383. package/dist/streams/events.js.map +1 -0
  384. package/dist/streams/index.d.ts +47 -0
  385. package/dist/streams/index.d.ts.map +1 -0
  386. package/dist/streams/index.js +47 -0
  387. package/dist/streams/index.js.map +1 -0
  388. package/dist/streams/realtime.d.ts +27 -0
  389. package/dist/streams/realtime.d.ts.map +1 -0
  390. package/dist/streams/realtime.js +50 -0
  391. package/dist/streams/realtime.js.map +1 -0
  392. package/dist/streams/state.d.ts +26 -0
  393. package/dist/streams/state.d.ts.map +1 -0
  394. package/dist/streams/state.js +34 -0
  395. package/dist/streams/state.js.map +1 -0
  396. package/dist/users/index.d.ts +9 -0
  397. package/dist/users/index.d.ts.map +1 -0
  398. package/dist/users/index.js +23 -0
  399. package/dist/users/index.js.map +1 -0
  400. package/dist/users/service.d.ts +43 -0
  401. package/dist/users/service.d.ts.map +1 -0
  402. package/dist/users/service.js +113 -0
  403. package/dist/users/service.js.map +1 -0
  404. package/dist/webhooks/inbound.d.ts +70 -0
  405. package/dist/webhooks/inbound.d.ts.map +1 -0
  406. package/dist/webhooks/inbound.js +146 -0
  407. package/dist/webhooks/inbound.js.map +1 -0
  408. package/dist/webhooks/index.d.ts +12 -0
  409. package/dist/webhooks/index.d.ts.map +1 -0
  410. package/dist/webhooks/index.js +65 -0
  411. package/dist/webhooks/index.js.map +1 -0
  412. package/dist/webhooks/outgoing.d.ts +89 -0
  413. package/dist/webhooks/outgoing.d.ts.map +1 -0
  414. package/dist/webhooks/outgoing.js +375 -0
  415. package/dist/webhooks/outgoing.js.map +1 -0
  416. package/dist/webhooks/signature.d.ts +40 -0
  417. package/dist/webhooks/signature.d.ts.map +1 -0
  418. package/dist/webhooks/signature.js +77 -0
  419. package/dist/webhooks/signature.js.map +1 -0
  420. package/dist/workspaces/context.d.ts +30 -0
  421. package/dist/workspaces/context.d.ts.map +1 -0
  422. package/dist/workspaces/context.js +12 -0
  423. package/dist/workspaces/context.js.map +1 -0
  424. package/dist/workspaces/index.d.ts +5 -0
  425. package/dist/workspaces/index.d.ts.map +1 -0
  426. package/dist/workspaces/index.js +21 -0
  427. package/dist/workspaces/index.js.map +1 -0
  428. package/dist/workspaces/service.d.ts +126 -0
  429. package/dist/workspaces/service.d.ts.map +1 -0
  430. package/dist/workspaces/service.js +495 -0
  431. package/dist/workspaces/service.js.map +1 -0
  432. package/docs/README.md +71 -0
  433. package/docs/api-keys.md +184 -0
  434. package/docs/audit.md +198 -0
  435. package/docs/auth.md +196 -0
  436. package/docs/authorization.md +225 -0
  437. package/docs/billing.md +293 -0
  438. package/docs/cli.md +238 -0
  439. package/docs/config.md +163 -0
  440. package/docs/db.md +219 -0
  441. package/docs/email.md +222 -0
  442. package/docs/entitlements.md +194 -0
  443. package/docs/errors.md +158 -0
  444. package/docs/events.md +279 -0
  445. package/docs/feature-flags.md +237 -0
  446. package/docs/files.md +229 -0
  447. package/docs/jobs.md +288 -0
  448. package/docs/logger.md +269 -0
  449. package/docs/notifications.md +179 -0
  450. package/docs/publishing.md +128 -0
  451. package/docs/realtime.md +360 -0
  452. package/docs/storage.md +279 -0
  453. package/docs/streams.md +296 -0
  454. package/docs/users.md +117 -0
  455. package/docs/webhooks.md +272 -0
  456. package/docs/workspaces.md +269 -0
  457. package/drizzle/0000_init_users.sql +21 -0
  458. package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
  459. package/drizzle/0002_billing_entitlements.sql +97 -0
  460. package/drizzle/0003_events.sql +17 -0
  461. package/drizzle/0004_notifications_audit.sql +64 -0
  462. package/drizzle/0005_api_keys_webhooks.sql +77 -0
  463. package/drizzle/0006_feature_flags.sql +22 -0
  464. package/drizzle/0007_files.sql +23 -0
  465. package/drizzle/meta/0000_snapshot.json +160 -0
  466. package/drizzle/meta/0001_snapshot.json +837 -0
  467. package/drizzle/meta/0002_snapshot.json +1580 -0
  468. package/drizzle/meta/0003_snapshot.json +1751 -0
  469. package/drizzle/meta/0004_snapshot.json +2109 -0
  470. package/drizzle/meta/0005_snapshot.json +2719 -0
  471. package/drizzle/meta/0006_snapshot.json +2909 -0
  472. package/drizzle/meta/0007_snapshot.json +3113 -0
  473. package/drizzle/meta/_journal.json +62 -0
  474. package/package.json +180 -0
package/docs/cli.md ADDED
@@ -0,0 +1,238 @@
1
+ # CLI
2
+
3
+ ```bash
4
+ npx sc init
5
+ npx sc start
6
+ ```
7
+
8
+ > Orientation and design notes: [`src/cli/README.md`](../src/cli/README.md).
9
+
10
+ A utility CLI, shipped as the `sc` binary, for bootstrapping and running a project
11
+ built on this package. The original `saas-core` binary remains available for the
12
+ advanced development stack.
13
+
14
+ ---
15
+
16
+ ## `sc init`
17
+
18
+ Prompts for the project name and creates `docker-compose.yml` in the current
19
+ directory. The generated stack contains PostgreSQL 17, binds it to
20
+ `127.0.0.1:5432`, persists its data in a named volume, and includes a healthcheck.
21
+
22
+ Pass `--project <name>` to skip the prompt, `--postgres-port <port>` to select a
23
+ different host port, or `--force` to replace an existing Compose file.
24
+
25
+ ## `sc start`
26
+
27
+ Starts the generated Compose stack in the background and waits until PostgreSQL
28
+ is healthy:
29
+
30
+ ```bash
31
+ sc start
32
+ ```
33
+
34
+ Run `sc init` first. The command never searches parent directories, so it starts
35
+ only the Compose file owned by the current project.
36
+
37
+ ---
38
+
39
+ ## `saas-core dev up`
40
+
41
+ Generates `.saas-core/` if it is missing, starts the stack, and waits for every
42
+ healthcheck to pass before returning.
43
+
44
+ ```
45
+ > docker compose v2.29.0
46
+ > created ./.saas-core/docker-compose.yml
47
+ > created ./.saas-core/Caddyfile
48
+ > created ./.saas-core/.gitignore
49
+ > starting postgres, minio and caddy
50
+ ok stack up (project my-app)
51
+
52
+ Endpoints
53
+ app https://app.localhost -> localhost:3000
54
+ postgres localhost:5432
55
+ minio http://localhost:9000
56
+ minio console http://localhost:9001
57
+
58
+ Configuration
59
+ Load these through Infisical rather than a .env file:
60
+
61
+ APP_URL https://app.localhost
62
+ DATABASE_URL postgres://saas:saas@localhost:5432/saas_core
63
+ STORAGE_BUCKET saas-core-dev
64
+ STORAGE_REGION us-east-1
65
+ STORAGE_ENDPOINT http://localhost:9000
66
+ STORAGE_ACCESS_KEY_ID saascoredev
67
+ STORAGE_SECRET_ACCESS_KEY saascoredev
68
+ ```
69
+
70
+ ### What it starts, and what it does not
71
+
72
+ | Service | Why |
73
+ | --- | --- |
74
+ | PostgreSQL 17 | Durable state, and the realtime transport — `LISTEN`/`NOTIFY` |
75
+ | MinIO | S3-compatible store for [storage](./storage.md) and [files](./files.md) |
76
+ | Caddy | Local HTTPS in front of your dev server |
77
+
78
+ No Redis: [realtime](./realtime.md) runs on PostgreSQL and [jobs](./jobs.md) on
79
+ Trigger.dev, so there is nothing else to operate. No mail catcher: the
80
+ [email](./email.md) module has a console provider for development.
81
+
82
+ ### Why Caddy is not optional
83
+
84
+ Three things differ over plain HTTP, and all three stay invisible until deploy:
85
+
86
+ - [`auth`](./auth.md) marks session cookies `Secure` outside development.
87
+ - `SameSite` enforcement differs between schemes.
88
+ - WorkOS redirect URIs are easier to keep consistent when both environments are
89
+ `https`.
90
+
91
+ `tls internal` issues a certificate from Caddy's own CA, so local matches
92
+ production. Trust the root once and the browser stops warning — `up` prints the
93
+ command to extract it.
94
+
95
+ ---
96
+
97
+ ## Commands
98
+
99
+ | Command | Effect |
100
+ | --- | --- |
101
+ | `dev up` | Generate, start, wait for health |
102
+ | `dev down` | Stop; **data preserved** |
103
+ | `dev down --volumes` | Stop and delete volumes — the one destructive command |
104
+ | `dev status` (`dev ps`) | Container state |
105
+ | `dev logs [service]` | Last 100 lines; `--follow` to stream |
106
+ | `dev init` | Write the files without starting anything |
107
+
108
+ ## Options
109
+
110
+ | Option | Default |
111
+ | --- | --- |
112
+ | `--project <name>` | the directory name, normalised |
113
+ | `--domain <host>` | `app.localhost` |
114
+ | `--app-port <n>` | `3000` |
115
+ | `--postgres-port <n>` | `5432` |
116
+ | `--minio-port <n>` | `9000`; the console takes the next port |
117
+ | `--force` | off — overwrite generated files |
118
+ | `--follow`, `-f` | off — follow logs |
119
+ | `--volumes` | off — with `down`, delete volumes |
120
+ | `--help`, `-h` / `--version` | |
121
+
122
+ Ports are validated before Docker sees them, so a typo fails with a message rather
123
+ than a bind error.
124
+
125
+ The project name defaults to the directory name, so two checkouts on one machine
126
+ get separate containers and volumes without anyone configuring it.
127
+
128
+ ---
129
+
130
+ ## The generated files
131
+
132
+ `.saas-core/docker-compose.yml` and `.saas-core/Caddyfile` are **meant to be
133
+ committed**. They are infrastructure, and a change to either should show up in a
134
+ diff.
135
+
136
+ They are generated rather than vendored because ports collide: a developer with an
137
+ existing PostgreSQL on 5432 needs a different one, and editing a vendored file
138
+ means losing the edit on every upgrade.
139
+
140
+ **Existing files are left alone.** A second `up` will not discard a Caddyfile you
141
+ added a second host to. `--force` overwrites.
142
+
143
+ `.saas-core/.gitignore` ignores only local state — logs — never the two
144
+ infrastructure files.
145
+
146
+ ### Choices baked into the compose file
147
+
148
+ **Every published port binds to `127.0.0.1`,** not `0.0.0.0`. Otherwise a laptop
149
+ on a cafe network is serving an unauthenticated database to the room. A test walks
150
+ every port mapping in the generated file and asserts it.
151
+
152
+ **`up --wait`.** Compose blocks on the healthchecks (`pg_isready`, `mc ready`), so
153
+ the command returning means the stack is usable — not merely created. Without it
154
+ the next command races the database.
155
+
156
+ **PostgreSQL pinned to 17; 15 is the floor.** The schema uses `UNIQUE NULLS NOT
157
+ DISTINCT` for [feature flag](./feature-flags.md) overrides, which does not exist
158
+ before 15. Pinning the major keeps a local database from silently diverging from
159
+ production, and a test asserts it.
160
+
161
+ **A one-shot `minio-init`** creates the bucket. MinIO starts with none, so without
162
+ it every upload fails on a fresh volume.
163
+
164
+ **`host.docker.internal:host-gateway`** is mapped explicitly. Caddy proxies to the
165
+ dev server on the *host* — so hot reload and the real `node_modules` keep working —
166
+ and Docker Desktop provides that name while Linux needs the mapping.
167
+
168
+ **Credentials are fixed and weak.** `saas:saas`, `saascoredev`. Throwaway data on
169
+ loopback; a generated password is one more thing to look up before running `psql`.
170
+
171
+ ---
172
+
173
+ ## Configuration is printed, not written
174
+
175
+ There is deliberately no `--write-env`. Configuration comes from Infisical (see
176
+ [config](./config.md)), and a `.env` file sitting in the project is exactly the
177
+ habit that leads to one in production.
178
+
179
+ `up` prints the values and the matching `infisical secrets set` commands, so the
180
+ next step is a paste rather than a lookup.
181
+
182
+ The variable names are checked against `src/config/sections.ts` by a test — a
183
+ drifted name would otherwise be silent: the stack comes up and the application
184
+ cannot see it.
185
+
186
+ ---
187
+
188
+ ## Error handling
189
+
190
+ Docker is checked before anything else, because "command not found", "daemon not
191
+ running" and "no compose plugin" are three problems with three different fixes:
192
+
193
+ ```
194
+ x The Docker CLI is installed but its daemon is not reachable.
195
+ Start Docker Desktop, or run `sudo systemctl start docker`.
196
+ ```
197
+
198
+ **The daemon needs its own probe.** `docker compose version` reports the *client*
199
+ plugin and succeeds with the daemon stopped — so checking only that lets a stopped
200
+ daemon through to fail halfway through an image pull. That was a real bug during
201
+ development; there is a test pinning the two-probe sequence.
202
+
203
+ Compose failures are translated where the cause is knowable — a port collision
204
+ names `--postgres-port`, exhausted disk suggests `docker system prune` — and passed
205
+ through verbatim otherwise. Anything unrecognised is better shown raw than
206
+ mislabelled.
207
+
208
+ Errors print a message and a hint, never a stack trace: a stack tells someone
209
+ running a CLI nothing actionable and buries the one line that matters.
210
+
211
+ ---
212
+
213
+ ## No CLI dependency
214
+
215
+ Node's `parseArgs` handles arguments; four escape codes and a TTY check handle
216
+ colour. Commander and chalk would each be a dependency for under twenty lines, in
217
+ a package many applications install.
218
+
219
+ Colour is dropped when stdout is not a TTY, when `NO_COLOR` is set, or when `TERM`
220
+ is `dumb` — escape codes in a CI log are noise a human then reads around.
221
+
222
+ ---
223
+
224
+ ## Testing
225
+
226
+ `CommandRunner` is a one-method interface, so every argument list is asserted
227
+ without Docker installed:
228
+
229
+ ```ts
230
+ const { runner, calls } = fakeRunner();
231
+ await devUp({ cwd, runner, projectName: "demo" });
232
+
233
+ expect(calls.find((c) => c.args.includes("up"))?.args).toContain("--wait");
234
+ ```
235
+
236
+ That covers template generation, port validation, file-preservation behaviour,
237
+ compose argument construction, and every error path. What it cannot cover is a real
238
+ container coming up — see the note in the root README.
package/docs/config.md ADDED
@@ -0,0 +1,163 @@
1
+ # Configuration
2
+
3
+ ```ts
4
+ import { config, assertConfig, ConfigError } from "@digimuza/saas-core/config";
5
+ import { clientConfig } from "@digimuza/saas-core/config/client";
6
+ ```
7
+
8
+ > Orientation and recipes: [`src/config/README.md`](../src/config/README.md).
9
+
10
+
11
+ Typed, Zod-validated configuration. Values come from the process environment,
12
+ which **Infisical** populates at runtime — there is no `.env` strategy:
13
+
14
+ ```bash
15
+ infisical run -- npm run dev
16
+ ```
17
+
18
+ Only two files in the whole package read `process.env`
19
+ (`src/config/env.ts` and `src/config/client.ts`), and a test enforces that.
20
+ Application code must never read it directly.
21
+
22
+ ---
23
+
24
+ ## `config`
25
+
26
+ An object of lazily-validated sections.
27
+
28
+ ```ts
29
+ config.app; // { name, url, environment, logLevel, isProduction }
30
+ config.database; // { url, maxConnections, ssl, statementTimeoutMs, preparedStatements }
31
+ config.auth; // { workosApiKey, workosClientId, cookiePassword, redirectUri }
32
+ config.billing; // { apiKey, webhookSecret, apiUrl }
33
+ config.email; // { apiKey, fromAddress, fromName }
34
+ config.storage; // { bucket, region, accessKeyId, secretAccessKey, endpoint, publicUrl }
35
+ config.realtime; // { url, apiKey }
36
+ config.jobs; // { secretKey, apiUrl }
37
+ ```
38
+
39
+ ### Why sections validate on first access, not at import
40
+
41
+ A core package is consumed by many runtimes — a migration script, a Trigger.dev
42
+ task, a Next.js route handler — and few need every integration configured.
43
+ Validating everything at import would force every deployment to supply Revolut
44
+ and storage credentials just to run a migration.
45
+
46
+ Each section is validated once, on first read, then memoised.
47
+
48
+ ---
49
+
50
+ ## `assertConfig(names?)`
51
+
52
+ ```ts
53
+ function assertConfig(names?: readonly SectionName[]): void
54
+ ```
55
+
56
+ Eagerly validate sections so misconfiguration surfaces at boot rather than
57
+ mid-request. Call it once at startup with what the process actually needs:
58
+
59
+ ```ts
60
+ assertConfig(["app", "database", "auth"]);
61
+ ```
62
+
63
+ - Throws `ConfigError` when one section is invalid.
64
+ - Throws `AggregateError` of `ConfigError` when several are, so one boot reports
65
+ every problem instead of making you fix them one at a time.
66
+ - Called with no argument, validates all eight sections.
67
+
68
+ ---
69
+
70
+ ## `ConfigError`
71
+
72
+ ```ts
73
+ class ConfigError extends Error {
74
+ readonly section: string; // "database"
75
+ readonly variables: readonly string[]; // ["DATABASE_URL"]
76
+ }
77
+ ```
78
+
79
+ The message names the offending **variables** and Zod's structural complaint, and
80
+ **never interpolates values** — so a malformed secret cannot leak into logs,
81
+ stack traces, or an error tracker. There is a test that puts a recognisable
82
+ secret into a failing config and asserts it does not appear in the message.
83
+
84
+ ---
85
+
86
+ ## Environment variables
87
+
88
+ | Section | Variables |
89
+ | --- | --- |
90
+ | `app` | `APP_NAME`, `APP_URL`, `APP_ENV`¹, `LOG_LEVEL`¹ |
91
+ | `database` | `DATABASE_URL`, `DATABASE_MAX_CONNECTIONS`¹, `DATABASE_SSL`¹, `DATABASE_STATEMENT_TIMEOUT_MS`¹, `DATABASE_PREPARED_STATEMENTS`¹ |
92
+ | `auth` | `WORKOS_API_KEY`, `WORKOS_CLIENT_ID`, `WORKOS_COOKIE_PASSWORD`², `WORKOS_REDIRECT_URI` |
93
+ | `billing` | `REVOLUT_API_KEY`, `REVOLUT_WEBHOOK_SECRET`, `REVOLUT_API_URL`¹ |
94
+ | `email` | `EMAIL_API_KEY`, `EMAIL_FROM_ADDRESS`, `EMAIL_FROM_NAME`³ |
95
+ | `storage` | `STORAGE_BUCKET`, `STORAGE_REGION`, `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_ACCESS_KEY`, `STORAGE_ENDPOINT`³, `STORAGE_PUBLIC_URL`³ |
96
+ | `realtime` | `REALTIME_URL`, `REALTIME_API_KEY` |
97
+ | `jobs` | `TRIGGER_SECRET_KEY`, `TRIGGER_API_URL`¹ |
98
+
99
+ ¹ has a default · ² minimum 32 characters · ³ optional
100
+
101
+ **Empty strings count as unset.** Secret managers and CI runners routinely inject
102
+ `FOO=` for absent values; without this, `.default()` and `.optional()` would never
103
+ apply and every deployment would need a placeholder for everything.
104
+
105
+ `DATABASE_PREPARED_STATEMENTS=false` is required behind a transaction-pooling
106
+ proxy (pgBouncer, Supabase or Neon pooler): prepared statements are per-session
107
+ and break when the proxy multiplexes sessions across connections.
108
+
109
+ Variables are named after the credential's **provider** (`REVOLUT_API_KEY`, not
110
+ `BILLING_API_KEY`) so an operator can see what belongs where, and so two
111
+ providers can run side by side during a migration.
112
+
113
+ ---
114
+
115
+ ## Client configuration
116
+
117
+ ```ts
118
+ import { clientConfig, assertClientConfig } from "@digimuza/saas-core/config/client";
119
+
120
+ clientConfig.app; // { name, url, environment, isProduction }
121
+ clientConfig.auth; // { workosClientId }
122
+ clientConfig.realtime; // { url }
123
+ ```
124
+
125
+ Required: `NEXT_PUBLIC_APP_NAME`, `NEXT_PUBLIC_APP_URL`,
126
+ `NEXT_PUBLIC_WORKOS_CLIENT_ID`. Optional: `NEXT_PUBLIC_APP_ENV` (defaults),
127
+ `NEXT_PUBLIC_REALTIME_URL`.
128
+
129
+ Three things make this safe:
130
+
131
+ 1. `config/client` imports **no** server configuration code, so a bundler cannot
132
+ reach a secret through it. Tested.
133
+ 2. Every variable is read as a **literal** `process.env.NEXT_PUBLIC_*` access.
134
+ Next.js substitutes those textually at build time; a dynamic
135
+ `process.env[key]` lookup is *not* substituted and would be `undefined` in the
136
+ browser. Do not refactor them into a loop — there is a test that fails if you
137
+ do.
138
+ 3. `@digimuza/saas-core/config` and `/db` throw if evaluated in a browser.
139
+
140
+ ### Why not the `server-only` package
141
+
142
+ `server-only` resolves to a module that *throws* unless the bundler sets the
143
+ `react-server` export condition. That condition is absent in plain Node, so
144
+ importing it would break every migration script, Trigger.dev task and Vitest run.
145
+ The core uses a runtime browser guard instead. This is weaker than build-time
146
+ enforcement — the structural guarantee is that clients import `/config/client`,
147
+ which never imports server code.
148
+
149
+ ---
150
+
151
+ ## Testing
152
+
153
+ ```ts
154
+ import { setEnvSource, resetConfigCache } from "@digimuza/saas-core/config";
155
+
156
+ resetConfigCache();
157
+ setEnvSource({ DATABASE_URL: "postgres://localhost/test" });
158
+ // ...
159
+ setEnvSource(undefined); // back to process.env
160
+ ```
161
+
162
+ Vitest merges into the ambient environment rather than replacing it, so tests must
163
+ inject a fixture rather than depend on `process.env`.
package/docs/db.md ADDED
@@ -0,0 +1,219 @@
1
+ # Database & schema
2
+
3
+ ```ts
4
+ import { getDatabase, createDatabase, runMigrations } from "@digimuza/saas-core/db";
5
+ import { workspaces, users } from "@digimuza/saas-core/db/schema";
6
+ ```
7
+
8
+ > Orientation and recipes: [`src/db/README.md`](../src/db/README.md).
9
+
10
+
11
+ PostgreSQL + Drizzle ORM. Two separate entry points, deliberately:
12
+
13
+ - **`/db/schema`** — table definitions only. No configuration, no driver. Safe for
14
+ drizzle-kit and for your own schema files. A test enforces that it imports
15
+ nothing but `drizzle-orm`.
16
+ - **`/db`** — the client. Server-only, needs configuration.
17
+
18
+ ---
19
+
20
+ ## `getDatabase()`
21
+
22
+ ```ts
23
+ function getDatabase(): Database
24
+ ```
25
+
26
+ The shared, configuration-driven handle. Reads the `database` config section on
27
+ first call, so it throws `ConfigError` if the database is not configured.
28
+
29
+ ```ts
30
+ const db = getDatabase();
31
+ await db.select().from(users);
32
+ ```
33
+
34
+ The handle is cached on `globalThis`. That is not premature optimisation: Next.js
35
+ dev-mode hot reloading re-evaluates modules on every edit, and without the cache
36
+ each edit would leak a connection pool until PostgreSQL refuses new connections.
37
+
38
+ ## `getDatabaseHandle()`
39
+
40
+ ```ts
41
+ function getDatabaseHandle(): DatabaseHandle // { db, client, close() }
42
+ ```
43
+
44
+ As above, but exposes the raw postgres.js client — for `LISTEN`/`NOTIFY` and raw
45
+ SQL — and the lifecycle.
46
+
47
+ ## `createDatabase(options)`
48
+
49
+ ```ts
50
+ function createDatabase(options: {
51
+ url: string;
52
+ maxConnections?: number; // 10
53
+ ssl?: boolean; // false
54
+ statementTimeoutMs?: number; // 30_000
55
+ preparedStatements?: boolean; // true
56
+ }): DatabaseHandle
57
+ ```
58
+
59
+ An explicitly configured handle, for when the caller must own the connection
60
+ lifecycle: migrations, tests, one-off scripts.
61
+
62
+ `statement_timeout` is applied per connection so a runaway query cannot hold a
63
+ pool slot indefinitely.
64
+
65
+ ## `closeDatabase()`
66
+
67
+ ```ts
68
+ function closeDatabase(): Promise<void>
69
+ ```
70
+
71
+ Close and forget the shared handle. For shutdown hooks and tests.
72
+
73
+ ## `CoreDatabase`
74
+
75
+ ```ts
76
+ import type { CoreDatabase } from "@digimuza/saas-core/db";
77
+ ```
78
+
79
+ A driver-independent handle typed against the core schema. Every service accepts
80
+ this rather than a concrete `PostgresJsDatabase`, so the same code runs against
81
+ postgres.js in production and PGlite in tests. A `PgTransaction` is assignable to
82
+ it, so any service also composes inside a transaction. There is a compile-time
83
+ assertion proving both drivers unify.
84
+
85
+ ---
86
+
87
+ ## Referencing core tables
88
+
89
+ ```ts
90
+ import { pgTable, text, uuid } from "drizzle-orm/pg-core";
91
+ import { workspaces } from "@digimuza/saas-core/db/schema";
92
+
93
+ export const projects = pgTable("projects", {
94
+ id: uuid("id").defaultRandom().primaryKey(),
95
+ workspaceId: uuid("workspace_id")
96
+ .notNull()
97
+ .references(() => workspaces.id, { onDelete: "cascade" }),
98
+ name: text("name").notNull(),
99
+ });
100
+ ```
101
+
102
+ `drizzle-orm` is a **peer** dependency for exactly this reason: two copies in one
103
+ dependency tree produce table objects Drizzle cannot relate to each other, and the
104
+ failure is a confusing type error far from the cause.
105
+
106
+ ---
107
+
108
+ ## Migrations
109
+
110
+ Versioned migrations ship inside the package (`files: ["drizzle"]`), so an
111
+ application applies core schema changes without vendoring SQL.
112
+
113
+ ```ts
114
+ import { runMigrations, coreMigrationsFolder } from "@digimuza/saas-core/db";
115
+
116
+ await runMigrations(); // core migrations
117
+ await runMigrations({ migrationsFolder: "./drizzle" }); // your own
118
+ ```
119
+
120
+ ```bash
121
+ npm run db:generate # after editing src/db/schema
122
+ infisical run -- npm run db:migrate
123
+ ```
124
+
125
+ Migrations run on a dedicated single connection: they take advisory locks and must
126
+ not compete with application traffic.
127
+
128
+ ---
129
+
130
+ ## Schema conventions
131
+
132
+ Follow these in your own tables so both halves of the schema stay coherent.
133
+
134
+ - **Explicit `snake_case` names everywhere.** Nothing relies on Drizzle's casing
135
+ inference, so a rename is always a visible diff.
136
+ - **UUID primary keys** from `gen_random_uuid()`. Identifiers appear in URLs;
137
+ sequential ids leak volume and allow enumeration across tenants.
138
+ - **`timestamptz` for all timestamps.** Never naive local time.
139
+ - **`updated_at` is maintained by Drizzle, not a trigger.** Writes that bypass the
140
+ application (manual SQL, data repairs) then show a stale timestamp rather than
141
+ being silently rewritten.
142
+
143
+ Helpers:
144
+
145
+ ```ts
146
+ import { primaryId, timestamps } from "@digimuza/saas-core/db/schema";
147
+
148
+ export const documents = pgTable("documents", {
149
+ id: primaryId(),
150
+ ...timestamps, // createdAt, updatedAt
151
+ });
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Constraint error introspection
157
+
158
+ ```ts
159
+ import { isUniqueViolation, isForeignKeyViolation, getPostgresError } from "@digimuza/saas-core/db";
160
+ ```
161
+
162
+ Tenant isolation in this schema *is* a set of database constraints, so services
163
+ need to tell "duplicate slug" from "cross-workspace role" from an unrelated
164
+ failure.
165
+
166
+ ```ts
167
+ try {
168
+ await db.insert(workspaces).values({ name, slug });
169
+ } catch (error) {
170
+ if (isUniqueViolation(error, "workspaces_slug_unique")) {
171
+ throw new ConflictError("Slug taken.");
172
+ }
173
+ throw error;
174
+ }
175
+ ```
176
+
177
+ Always pass the constraint name. Matching on the code alone will absorb unrelated
178
+ violations — a bug the core itself had: `ensureCustomer` treated *any* unique
179
+ violation as "a concurrent request won the race", which silently swallowed a
180
+ provider returning an id already bound to another workspace.
181
+
182
+ The helper walks the error's `cause` chain and accepts both spellings, because
183
+ postgres.js puts `code`/`constraint_name` on the thrown error while Drizzle's
184
+ PGlite driver nests `code`/`constraint` under `cause`.
185
+
186
+ ---
187
+
188
+ ## Table ownership
189
+
190
+ Core-owned: `users`, `user_profiles`, `workspaces`, `workspace_members`,
191
+ `workspace_invitations`, `roles`, `permissions`, `role_permissions`,
192
+ `workspace_member_roles`, `plans`, `entitlements`, `billing_customers`,
193
+ `subscriptions`, `billing_events`, `usage_records`, `events`.
194
+
195
+ Your application owns everything else. Do not add product-specific models to the
196
+ core unless they clearly apply to several products.
197
+
198
+ ---
199
+
200
+ ## Testing against real PostgreSQL
201
+
202
+ The core's own tests use `@electric-sql/pglite` — PostgreSQL compiled to WASM — and
203
+ run the real migrations:
204
+
205
+ ```ts
206
+ import { PGlite } from "@electric-sql/pglite";
207
+ import { drizzle } from "drizzle-orm/pglite";
208
+ import { migrate } from "drizzle-orm/pglite/migrator";
209
+ import { coreMigrationsFolder } from "@digimuza/saas-core/db";
210
+ import * as schema from "@digimuza/saas-core/db/schema";
211
+
212
+ const client = new PGlite();
213
+ const db = drizzle(client, { schema });
214
+ await migrate(db, { migrationsFolder: coreMigrationsFolder });
215
+ ```
216
+
217
+ Real PostgreSQL rather than a mock, because the guarantees under test *are*
218
+ database constraints — composite foreign keys, partial unique indexes, cascades. A
219
+ fake would happily accept the cross-tenant writes the schema exists to reject.