Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.15.1"
".": "1.15.2"
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.15.2](https://github.com/trycompai/crm/compare/v1.15.1...v1.15.2) (2026-08-20)


### Documentation

* **api:** explain runtime openapi document and vendoring rules ([#170](https://github.com/trycompai/crm/issues/170)) ([630f2c9](https://github.com/trycompai/crm/commit/630f2c9f375b5ae084ed708d21bcdb563c774a4f))

## [1.15.1](https://github.com/trycompai/crm/compare/v1.15.0...v1.15.1) (2026-08-20)


Expand Down
23 changes: 21 additions & 2 deletions apps/api/scripts/build-func.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ const EXTERNALS = [
"cardinal",
];

const VENDOR_ROOTS = ["express", "@nestjs/swagger", "reflect-metadata"];
const VENDOR_ROOTS = [
"express",
"@nestjs/swagger",
"reflect-metadata",
"@nestjs/common",
"@nestjs/core",
"class-transformer",
"class-validator",
];

rmSync(outDir, { recursive: true, force: true });
mkdirSync(funcDir, { recursive: true });
Expand Down Expand Up @@ -102,7 +110,18 @@ function vendorDeps(realDir, name, placedDir) {
} catch {
return;
}
for (const depName of Object.keys(pj.dependencies || {})) {
const optionalPeers = new Set(
Object.entries(pj.peerDependenciesMeta || {})
.filter(([, meta]) => meta.optional)
.map(([depName]) => depName),
);
const depNames = new Set([
...Object.keys(pj.dependencies || {}),
...Object.keys(pj.peerDependencies || {}).filter(
(depName) => !optionalPeers.has(depName),
),
]);
for (const depName of depNames) {
const depReal = resolveDep(realDir, name, depName);
if (!depReal) {
console.warn(` ! not found: ${depName} (needed by ${name})`);
Expand Down
22 changes: 22 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ self-hoster's admin cannot redeploy.
Only `check-types` and `dev` run it. If the app cannot see a new procedure, it has
not run.

## The OpenAPI document is built at runtime, not committed

`GET /openapi.json` serves one document: Nest's own controllers plus a REST bridge
under `/rest` generated from every tRPC procedure. Swagger UI renders it at `/`.
`createApp` builds both halves and merges them, so nothing is generated at build
time and no file is checked in — the document is whatever the routers are.

`SwaggerModule.setup` runs **before** `app.init()`, because it registers its Express
routes synchronously and Nest's own routing would otherwise shadow them. The factory
form defers building the document to the first request, which is what lets it read
the tRPC router that only exists after init.

Two rules follow for the serverless build:

- `@nestjs/swagger` stays in `EXTERNALS` in `apps/api/scripts/build-func.mjs`, because
`swagger-ui-dist` resolves its assets from disk at runtime and cannot be bundled.
- Anything external must be **vendored**, and vendoring follows non-optional
`peerDependencies`, not only `dependencies`. Nest packages declare their runtime
needs as peers, so following `dependencies` alone ships a function that throws
`MODULE_NOT_FOUND` on the first request. Adding a name to `EXTERNALS` without
checking it lands in `.vercel/output` breaks production, and the build stays green.

## Two mail providers, one pipeline

`apps/api/src/mailbox` is everything neither Google nor Microsoft owns:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "crm",
"private": true,
"license": "MIT",
"version": "1.15.1",
"version": "1.15.2",
"scripts": {
"prepare": "git config core.hooksPath .githooks 2>/dev/null || true",
"build": "turbo run build",
Expand Down