@gadicc/loom
v1.31.1
Published
Deno-first shared helpers for extracting common Next.js app code.
Readme
loom
Deno-first shared helpers for extracting common code from multiple Next.js apps.
This repo currently still lives in the gadicc directory, but the package name
is @gadicc/loom and the intended CLI name is loom.
Project status and support
Loom is primarily developed for the maintainer's own commercial and open source applications. Others are welcome to use it, but it is provided without warranty and without any commitment to support, compatibility, maintenance periods, or a particular roadmap. Releases follow semantic versioning so independently pinned consumer apps can upgrade deliberately.
Security issues should be reported privately as described in
SECURITY.md, not through a public issue.
Current feature set:
- shared DB stack scaffolding for Neon/Drizzle/Valibot/PGlite consumers
- shared jobs framework primitives for app-local
src/jobsimplementations - shared Next.js App Router jobs adapters for list pages, run pages, and route handlers
- auditable, scope-agnostic API-key authorization with an app-owned Better Auth adapter, managed audit schema, explicit jobs integration, optional MUI UI, Vercel country/IP snapshots, and bounded retention
- durable provider-neutral outgoing mail with atomic outbox leases, Postal transport and signed webhooks, per-recipient state, and admin-gated test mail
- a Loom-managed env runner for non-Next commands that need the same
.env*semantics as Next - consumer project tooling that supports
package.json,deno.json,deno.jsonc, and dual-config projects with separate task and dependency owners recorded inloom.json - Deno tests with BDD-style assertions
- npm package generation via
dnt
License
Copyright © 2026 Gadi Cohen. Licensed under the Apache License 2.0.
The license includes warranty disclaimers and a limitation of liability; consumers remain responsible for determining whether Loom is appropriate for their applications.
Loom does not redistribute third-party agent skills. Its metadata-only catalog records reviewed upstream revisions and license evidence; consumer projects choose and cache the corresponding content directly from upstream.
Public npm package
Loom's public distribution is @gadicc/loom on npm:
pnpm add @gadicc/loom
# or: npm install @gadicc/loomConsumer projects do not need registry mappings or package-read tokens.
Maintainer bootstrap, trusted-publisher configuration, release verification, and
consumer migration are documented in
docs/npm-publishing.md.
CLI
loom now includes a small bootstrap CLI and feature-scaffolding entrypoint.
The detection, dual-config precedence, and safe task-composition contract is
documented in docs/project-tooling.md.
loom initadds@gadicc/loomto the authoritative dependency config, creates or updatesloom.json, records the authoritative task runner/config and dependency manager/config, refreshes the managed Loom bootstrap block inAGENTS.md, and ensures the managed.agents/skills/*links point at the installed@gadicc/loompackage, activates skills exposed by installed npm packages, and configures version-matched Next.js guidance when supported by the installed Next version.loom addlists the shared features that the CLI can activate and shows a short description for each one.loom add <feature>activates one shared feature inloom.jsonand applies that feature's scaffolding without overwriting app-local wrappers.loom add authrecords app-owned Better Auth entrypoints, scaffolds missing thin client and route wrappers, and enables theloom-authskill link.loom add jobsactivates the shared jobs feature inloom.jsonand scaffolds any missing local jobs/admin route files without overwriting existing app-local wrappers, while also enabling theloom-jobsskill link.loom add dbactivates the shared DB feature inloom.json, adds the managed DB dependencies and tasks to their recorded configs, rewrites the standardneon.ts/neonFull.ts/drizzle.config.ts/ PGlite test harness files into Loom-managed thin wrappers, scaffolds the app-local fixture script entrypoint, and enables theloom-dbskill link.neonFullverifies remote TLS certificates and hostnames even when the selected URL containssslmode=require. Private remote CAs must be trusted explicitly, for example with process-startNODE_EXTRA_CA_CERTS; invalid certificates fail instead of falling back. The existing plaintext exceptions fordb.localtest.me,localhostand127.0.0.1remain, including their existing override of URL TLS settings. Database URL precedence is unchanged. TLS verification does not add SCRAM channel-binding support to the underlying driver. Remote IPv4 certificates must contain a matching IP subject alternative name. DNS-only multi-host URLs retain the driver's per-host verification. Empty or escaped hosts, extra unescaped@signs or fragments in the authority, IP-containing multi-host URLs and bracketed IPv6 hosts are rejected because the runtime cannot reliably bind their current driver routing to certificate identity; use an explicit DNS host or single IPv4 address.loom add filesactivates the shared files schema contract after the DB feature is active. It materializes the Loom-managed Drizzle schema blueprint into an app-local committed schema file, records the blueprint version and hash inloom.json, scaffolds app-local repository/storage/access/route files, and leaves migration generation to the app's normaldb:generatereview flow.loom add mailrequires active DB and auth features, materializes the message/delivery/event Drizzle schema, and scaffolds a working app-local repository, Postal transport, service, RSA-signed webhook route, fail-closed admin boundary, and (for MUI apps) mail log plus test/one-off-mail UI. When jobs is active it also scaffolds a drain job without rewriting app-owned catalogs or schedules. Loom never generates the migration.@gadicc/loom/mailowns provider-neutral enqueue/lease/retry/cancel/event orchestration and memory test adapters.@gadicc/loom/mail/postalowns the Postal HTTP and webhook contract,@gadicc/loom/next/mailowns the raw-body route handler, and@gadicc/loom/mui/mailowns optional serializable admin views. Keep the default sender structured with the required bare address[email protected]and optional display namePOSTAL_FROM_NAME=Example App. Configure Postal webhook verification with a JSON array of RSA public JWKs inPOSTAL_WEBHOOK_PUBLIC_KEYS; shared-secret emulation is unsupported.loom files r2 setupcreates or reuses private Cloudflare R2 buckets for Production and Preview, records the explicit Cloudflare account and storage policy inloom.json, replaces only the untouched generated storage stub, configures canonicalFILES_S3_*values in Vercel, and configures local MinIO in.env.localby default. Use--local-provider r2for deliberately shared development storage or--local-provider noneto leave local storage unwired. Existing custom storage modules and credentials are preserved.loom files r2 checkverifies the selected Cloudflare account, bucket existence and private access, exact non-secret Vercel values in Production and Preview, the absence of Files credentials in Vercel Development, and the configured local MinIO endpoint/bucket. The same checks run throughloom check --network.loom add api-keysrequires active auth and DB features, materializes the versioned audit schema, scaffolds fail-safe policy/verifier/repository/action boundaries and user/admin pages, and links theloom-api-keysskill. It does not authorize existing routes, change jobs, or generate migrations.@gadicc/loom/apiKeysprovides typed scope registries, exact-match policy dispatch, credential extraction, fingerprints, authorization/audit contracts, country normalization, and bounded audit retention.@gadicc/loom/next/apiKeysadds App Router authorization, session-or-key composition, completion auditing, and explicit Vercel request context.@gadicc/loom/mui/apiKeysprovides optional lifecycle and audit UI.loom api-keys integrate jobs --resource <name> --action <name>records an explicit additive jobs authorization integration after the app has protected its HTTP jobs route with fresh session/admin policy. Direct local runners are unchanged and remain API-key-free.loom api-keys retention enablerecords the default 180-day/5,000-request retention policy. The app must transactionally implement the retention repository and schedule the shared runner; Loom does not silently create or modify jobs. Useretention disablefor an app-owned legal-hold workflow.@gadicc/loom/filesprovides the shared files runtime primitives: SHA-256 hashing, digest verification, metadata detection, repository/storage adapter contracts, in-memory test adapters, and headless upload helpers.@gadicc/loom/files/s3adapts an app-provided AWS SDK v3 S3 client to the shared storage contract without making AWS SDK packages Loom dependencies.@gadicc/loom/next/filesprovides App Router route-handler factories for uploads, metadata lookup, and downloads over app-provided persistence and storage adapters.loom add linguiadopts Lingui catalog tooling without owning locale routing or translations. It records loader versus precompile mode, adds compatible dependencies and extract/compile tasks, ignores generated JavaScript catalogs, and ensures precompiled catalogs are built before the app build.loom checkalso reports tracked generated catalogs, task-order drift, mismatched runtime imports, and missing Next.js loader/SWC wiring.loom add pwaactivates the shared Serwist PWA feature, adds the Serwist dependencies and build task, safely composes it after an existing Next build, scaffolds configurator-modeserwist.config.mjsplus worker/provider files, records whether the update prompt should use the MUI adapter or browser fallback, adds a 60-second update check interval for MUI installs, and ignores generated service-worker output.loom add skillsadopts matching reviewed external skills into the gitignored.loom/skillscache, records exact revisions and content hashes inloom-skills.lock.json, and activates package-provided skills.loom statussummarizes the installed Loom package,loom.jsonstate, active features, local-dev sync state, and managed/recommended skill links.loom env -- <command> [...args]loads the consumer project.env*files with Next-compatible@next/envsemantics, preserves existing shell env precedence, then runs the command from the consumer project root. Use it for non-Next commands such as direct job runners, Drizzle, ad hoc DB inspection, and app scripts that need env. Next commands such asnext devandnext buildalready load.env*themselves.loom skills checkvalidates the project lock, external cache, and activation links.loom skills updateupdates adopted external skills to Loom's current reviewed immutable revisions.loom skills disable <name>opts out of automatic activation;enableclears the opt-out andsyncreactivates the skill.loom db fixturesruns the Loom-managed DB fixture dump entrypoint for the active DB feature, using the configuredscripts/dump-test-tables.tsand scripts tsconfig.loom db schema-genruns the Loom-managedvs_tocodeworkflow for the active DB feature and then refreshes the Valibot barrel file. Pass extra generator flags after--, for exampleloom db schema-gen -- --watch.loom db schemas syncre-materializes active Loom-managed schema blueprints into app-local schema files only when the file still matches the last recorded Loom hash. It refuses to overwrite local edits, updates simple schema barrel exports, and never generates Drizzle migrations.loom db neon setupprovisions a Vercel-managed Neon database for the active DB feature, connects it to the linked Vercel project for Production and Preview only, skips local env pulling, and records the provider policy inloom.json.loom db neon checkruns only the DB/Neon network checks: Vercel CLI availability, project linking, Neon resource connection, Production/Preview DB env vars, and the absence of DB env vars in Development.loom db release setup-githubcreates/updates the GitHubproductionenvironment for staged Vercel releases: it storesVERCEL_ORG_IDandVERCEL_PROJECT_ID, and saves a bounded project-scoped Vercel token as the GitHubVERCEL_TOKENenvironment secret. Reruns preserve an existing secret; use--rotate-tokenfor explicit replacement or--token-scope teamonly for compatibility. Its branch policy and the release checks usefeatures.db.config.release.productionBranchfromloom.json, defaulting tomain. See release configuration for other production branches and matching app-owned workflow/Vercel settings.loom db release prepare-vercel-project production <env> <metadata> <project>materializes a private, consistency-checked Production env snapshot and exact.vercel/project.jsonfor a project-scoped token. After a standalone build, stage the prebuilt artifact withVERCEL_ORG_IDandVERCEL_PROJECT_IDremoved from that deploy process, then useloom db release promote-vercel <deployment-id>for bounded project API promotion and terminal readback.loom dev local /path/to/loomenables local unpublished Loom development. It adds a managedfile:override inpnpm-workspace.yamlfor pnpm projects or a Denolinksentry indeno.json(c), then runs the local-dev sync flow when needed.loom dev syncrebuilds the local Loom package when source files are newer than the generated npm output, then refreshes the consuming app with the pnpm update/install flow ordeno installwhen the installed linked dependency is stale. Other Loom commands also run this sync automatically when the managed local-dev override is active; pass--no-syncto skip it. Each sync records the Loom source commit in the managedpnpm-workspace.yamlblock and gitignored.loom/dev-state.jsonwhen the local package path points at a Loom git checkout.loom dev publishedremoves that managed local-dev override so the app can return to the published package version. When a prior sync recorded a Loom source commit, this command verifies that the commit is reachable fromorigin/mainand either included in a semver release tag or package-equivalent to the latest semver release because only non-package files changed after that tag. It then switches the app to the exact published@gadicc/loomversion that covers the sync. If no recorded commit exists, the command preserves the older compatibility behavior and switches back to the existing published dependency with an explicit warning. The command also notices stale lockfiles that still resolve Loom to a local package even after the workspace override has been removed.loom checkverifies the recorded package/Deno configs,loom.json, the managedAGENTS.mdblock and.agents/skills/*links, and then runs feature-specific checks for active entries inloom.json, including jobs-wrapper validation plus DB runtime, script, Valibot-barrel wiring, and the production migration release flow for active DB installs, managed local schema validation for active files installs, and Serwist package, worker, provider, update-check interval, MUI-confirm, and build-script wiring for active PWA installs. The DB release-flow check expects Vercel automatic production Git deployments to be disabled invercel.jsonand a GitHub Actions workflow to pull Vercel production settings, build, upload a staged prebuilt production deployment, run a live migration credential preflight, rundb:migrateagainst the preserved production environment, then promote the staged deployment.loom check --networkalso verifies the required GitHub release secret/variables when that workflow exists. When.github/workflows/*.yml|yamlstill contains Loom's obsolete GitHub Packages registry/auth setup,loom checkrecommends removing it. The command also activates newly discovered npm package skills with a notice, warns when the managed local dev override is active, reports inlinedotenv/@next/envimports or DB/jobs tasks that bypassloom env --, warns when authoritative and secondary configs expose conflicting tasks, and warns when a Deno link orpnpm-lock.yamlstill resolves Loom to local source. If.loom/dev-state.jsonrecords a synced Loom commit,loom checkalso explains whether that commit is unpublished or published in a newer exact Loom version than the app is pinned to. Pass--strict-skillsto turn missing skill recommendations into check failures. Pass--productionto make local Loom dev overrides, local lockfile resolutions, unresolved synced Loom commits, and DB migration release-flow gaps release-blocking failures. Pass--networkto also run registered feature network checks after the local project checks.
The current CLI scope is project bootstrap, validation, and feature-specific
scaffolding. Extracted feature hooks are added behind the same init, add,
feature subcommand, and check flows.
Consumer app release workflow
Consumer apps may stay in local Loom development mode during normal feature work:
loom dev local /home/dragon/www/npm/loom
loom dev sync
loom checkBefore pushing or releasing a consumer app, switch back to the published Loom package and run the production gate:
loom dev published
pnpm update --no-save @gadicc/loom@<version-from-loom-output>
pnpm install
loom check --productionFor Deno-managed projects, replace the pnpm refresh commands with
deno install; loom dev published prints the project-appropriate command.
Publishing Loom is only required when the app was synced against package-relevant Loom changes that are not already covered by a published semver tag. Publishing a new Loom version does not move other consumer apps; each app remains on its committed dependency config and lockfile until that app deliberately upgrades.
New app DB bootstrap
For a new Vercel app that needs the shared DB stack:
loom init
loom add db
vercel link
loom db neon setup
loom check --networkloom db neon setup runs the Vercel Marketplace install through the Vercel CLI
using Production and Preview environments only. It intentionally avoids
Development and uses --no-env-pull so local .env* files are not populated by
the integration. Pass --name <resource-name> to override the default
<project>-db resource name, --region <region> to select a supported Neon
Marketplace region such as sin1, or --plan <plan-id> if a new Marketplace
installation requires a specific plan id. When Neon is already installed for the
team, Loom omits --plan and preserves that installation's current plan. Neon
Auth is explicitly disabled by default because Loom's auth feature uses Better
Auth; pass --neon-auth only when the app intentionally wants Neon Auth too.
Network checks are line-based for now so they remain readable in CI logs:
[1/6] Checking Vercel CLI is available ......................... ok ✓
Using vercel.
[2/6] Checking Project is linked to Vercel ..................... ok ✓
Linked to Vercel project prj_...When stdout is an interactive terminal, Loom uses an updating progress line with
a progress bar and prints only problem checks at the end. This is disabled
automatically in CI and can be disabled explicitly with --no-progress. Use
--no-color to disable colored status output.
See docs/network-checks.md for the hook design and
future expansion notes.
AI skills
The package now publishes Codex skill folders under skills/.
skills/loomprovides global loom guidance for consumer apps, centered onloom.json, feature activation, shared-vs-local ownership boundaries, and a small set of shared consumer-repo policies such as verification, documentation/comments, and commit-message expectations. Consumer repos keepAGENTS.mdsmall and use a managed Loom block there to route work into this shared skill.skills/loom-torvalds-reviewadapts the externallinus-torvalds-skillreview method to web apps, shared packages, CLI code, native code, and explicit prototypes. It keeps the upstream emphasis on correctness, compatibility, evidence, and simplicity while disabling literal kernel/C rules outside their real prerequisites.skills/upgrade-depsprovides a staged Deno and JavaScript/TypeScript dependency-upgrade workflow covering compatibility groups, rollback safety, failure isolation, major migrations, reviewable commits, and exact-tree final verification.skills/modernize-appguides staged modernization of an existing application, including identity/data preservation, optional offline/media transitions and verified rollout. Dependency-only work stays withupgrade-deps; new apps use scaffolding rather than this migration workflow.skills/loom-dbprovides DB-feature guidance for Neon/Drizzle wiring, managed local schema blueprints, Valibot generation, and the shared PGlite harness.skills/loom-authcovers Better Auth secrets, rotation, environment trust boundaries, and proxy-aware localhost OAuth configuration.skills/loom-jobsprovides jobs-feature guidance for app-local catalogs, admin route wrappers, and shared helper wiring.skills/loom-api-keysprovides API-key adoption, authorization, audit, Vercel geolocation, UI mapping, jobs integration, and retention guidance.
Projects do not need to copy these files manually. loom init, loom add db,
loom add jobs, and loom check manage a small project-local AGENTS.md Loom
block plus .agents/skills/* symlinks that point to the installed
node_modules/@gadicc/loom/skills/* folders. The global loom,
loom-torvalds-review, upgrade-deps, and modernize-app skills are linked
for every initialized project; feature skills are linked when their matching
feature is active.
Third-party skills are not packaged with Loom. Loom carries a metadata-only
catalog containing source URLs, immutable commits, content hashes, and license
evidence. loom add skills installs applicable reviewed skills directly into a
consumer project's gitignored .loom/skills cache and records the selection in
the committed loom-skills.lock.json. Missing or unclear licenses require the
explicit --accept-unknown-license <name> flag. loom skills migrate replaces
legacy symlinks from older Loom releases, while preserving custom local skill
directories.
Loom also scans installed npm packages for a root SKILL.md, skills/*, or
.agents/skills/*. New package-provided skills are activated automatically by
loom init, loom skills sync, and ordinary loom check, with a notice that
includes loom skills disable <name> as the opt-out path.
For local Loom package development, prefer loom dev local and the managed
file: marker flow over pnpm link. loom dev sync owns the rebuild and pnpm
refresh step for that local package. When it refreshes the installed package at
the start of another Loom command, rerun that original command so the current
process uses the refreshed CLI code. This keeps the committed dependency on the
published version while still resolving peer dependencies correctly during local
development. Treat this as a workaround for current pnpm behavior rather than a
permanent preference; if pnpm's local-link behavior changes, this flow can be
simplified or removed later.
The managed local-dev block also records the Loom source commit that was synced
when the local package path points at a Loom git checkout, and the same state is
persisted in gitignored .loom/dev-state.json so Loom can still diagnose stale
local lockfiles after the workspace override has been removed.
loom dev published uses that record as a release gate: clean commits that have
reached origin/main and are either included in a semver release tag or have no
package-relevant changes after the latest semver tag can be switched to the
exact published version, while dirty or unreleased package changes block the
switch. Existing apps with older local-dev blocks that lack this metadata can
still switch back to their current published dependency, but new syncs should be
used before relying on the release gate.
Loom's external catalog currently recommends these defaults:
ai-sdkfind-skillsfrontend-designlinus-torvalds-skill(with Loom's context-aware review companion)vercel-composition-patternsvercel-react-best-practices
It adds these dependency-matched recommendations:
better-auth-best-practicesfor projects that depend on Better Auth packagespnpmfor projects that use pnpmvitestfor projects that use Vitest
Next.js is handled separately because current Next releases ship documentation
matching the installed framework version under node_modules/next/dist/docs.
Loom adds a managed AGENTS.md pointer to those docs. Older Next releases can
use the official npx @next/codemod@latest agents-md fallback; migration
removes the retired next-best-practices symlink.
See docs/project-skills.md for lock/cache semantics,
package discovery, opt-outs, collision behavior, and migration details.
Jobs framework
The shared jobs code is designed so each app keeps its own jobs in
src/jobs/*.ts, while the shared package owns the common controller, catalog,
execution, formatting, and direct-run helpers. Keep metadata in a
dependency-free module and use createLazyJob in the executable catalog so
constructing an HTTP or direct-run catalog does not initialize every job's
transitive dependencies.
Example app-local job:
import type { Job } from "@gadicc/loom/jobs";
import { testMeta as meta } from "./indexWithMetaOnly";
export { meta };
export const handler: Job["handler"] = async (job) => {
job.log("hello");
};Example dependency-free metadata and lazy catalog entry:
// indexWithMetaOnly.ts
import type { Job } from "@gadicc/loom/jobs";
export const testMeta: Job["meta"] = {
id: "test",
desc: "Example job",
};
// index.ts
import { createLazyJob } from "@gadicc/loom/jobs";
import { testMeta } from "./indexWithMetaOnly";
export const test = createLazyJob(testMeta, () => import("./test"));Job output can be sanitized once at the execution boundary so the same policy applies to NDJSON, text, HTML, direct runners, HTTP responses, and app-provided formatters:
import { createJobOutputRedactor, type Job } from "@gadicc/loom/jobs";
import { createJobsRouteHandler } from "@gadicc/loom/next/jobs/route";
const providerToken = process.env.PROVIDER_TOKEN;
if (!providerToken) throw new Error("PROVIDER_TOKEN is required");
const outputSanitizer = createJobOutputRedactor(
new Map<string | RegExp, true | string>([
[providerToken, true],
[/Bearer\s+\S+/gi, "Bearer [REDACTED]"],
]),
);
export const jobsRouteHandler = createJobsRouteHandler({
catalog: jobs as readonly Job[],
handlerErrorMessage: "Job execution failed.",
outputSanitizer,
});Redaction rules run in map insertion order, each against the preceding rule's
result. Exact strings match wherever they occur. RegExp rules replace every
match without using the caller's lastIndex, even when the expression is
non-global or sticky. A true replacement means [REDACTED]; string
replacements are literal, so $&, $1, and $$ are not expanded. Empty exact
matchers are rejected without printing configured match or replacement values.
The sanitizer visits every string value emitted through job.log(), all other
log levels, and job.json() while retaining normal JSON behavior for arrays,
objects, dates, numbers, booleans, and null. Inputs are not mutated. If a
sanitizer throws or returns a non-string, Loom fails the execution closed and
emits only Job output sanitation failed. for that failure.
Output redaction and unexpected-error disclosure are separate. Omitting
handlerErrorMessage retains Loom's existing full-stack fatal event; supplying
a string emits that exact generic literal instead. Deferred-task errors keep
their own deferredTaskErrorHandler, and the additive
runDeferredTasksWithResult() method returns a failure count while
runDeferredTasks() keeps its legacy Promise<void> contract. Consumers that
run deferred tasks inline can emit a generic event from that callback; tasks run
after a response stream closes should report through a separate safe sink.
cancellationMessage: false suppresses the legacy Job cancelled event when
cancellation must only set the controller flag.
This boundary protects Loom job-event output only. It does not sanitize
console.*, provider or framework logs, audit metadata, request URLs, thrown
values retained in execution.result, or external logging systems. Object
property names are JSON structure rather than string values and are not
rewritten; do not place secrets in keys.
For the strongest Next.js deployment boundary, mount jobs in static route files
with createJobRouteHandler({ job }). A dynamic [id] route is one serverless
function and therefore still traces every implementation reachable from its
catalog, even when createLazyJob prevents unrelated modules from evaluating at
runtime.
Loom can manage those file-based entrypoints from the Jobs config in
loom.json:
{
"staticRoutes": {
"handlerFactoryExport": "createHttpJobRouteHandler",
"jobs": {
"example": "@/jobs/example",
"short-example": {
"module": "@/jobs/short-example",
"maxDuration": 300
}
},
"maxDuration": 1800,
"routeDirectory": "src/app/admin/jobs"
}
}Run loom add jobs after changing the map. Loom creates, updates, and removes
only wrappers bearing its managed marker; an existing unmanaged route is
preserved and reported. loom check verifies the generated set and its direct
literal maxDuration exports. The default is Vercel's current 30-minute maximum
of 1800, while staticRoutes.maxDuration and each job's optional
maxDuration override keep shorter limits explicit and checkable.
