Reusable NestJS core, extracted from badthingsforpets.com: auth (GitHub/Google OAuth + standalone work-email sign-in), organizational-email professional verification, a DynamoDB client module, and SSM-backed configuration management. No process.env reads inside @bubltec/mycota-auth or @bubltec/mycota-config — everything is passed in explicitly, so a consuming app supplies its own secrets/table names/callback URLs instead of inheriting anyone else's.
A pnpm + turbo monorepo (same shape as btfp itself), so each concern is its own versionable package instead of one flat bundle — a project that only wants dynamo isn't forced to pull in passport/bedrock/ses too, and there's room to add more packages as the framework's reach grows.
packages/dynamo→@bubltec/mycota-dynamo—DynamoModule(global, points at DynamoDB Local whenDYNAMODB_ENDPOINTis set),DYNAMO_DOC_CLIENT,stripDynamoKeys. No internal dependencies.packages/config→@bubltec/mycota-config— SSM Parameter Store-backed configuration, namespaced by app and environment. No internal dependencies. See below.packages/auth→@bubltec/mycota-auth—MycotaAuthModule.forRootAsync({ useFactory }),JwtAuthGuard,VerifiedGuard,CurrentUser,UsersService,EmailCodeService, plus the SSM conveniencebuildMycotaAuthConfigFromSsm. Depends on@bubltec/mycota-dynamo,@bubltec/mycota-config, and@bubltec/mycota-mail.packages/professional-verification→@bubltec/mycota-professional-verification— request/confirm/review workflow for "prove you belong to an organization," built on@bubltec/mycota-auth's email-code flow. Depends on@bubltec/mycota-auth.packages/cdk→@bubltec/mycota-cdk— CDK constructs:grantSsmConfigRead,EphemeralConfig,MediaBucket(private S3 + CloudFront OAC),JobQueue(SQS + DLQ + EventBridge Scheduler group),PostgresInstance(RDS Postgres 16; app brings the VPC),SesDomain(verified sending identity + Easy DKIM + MAIL FROM), andGithubActionsDeployRole(GitHub OIDC assume-role forcdk deploy). Depends on@bubltec/mycota-config;aws-cdk-lib/constructsare peer dependencies (bring your own pinned CDK version). See below.packages/payments→@bubltec/mycota-payments—PaymentGatewayport,FakePaymentGatewayfor local/tests, and aStripePaymentGatewaythat destination-charges Stripe Connect accounts.ConnectOnboarding+FakeConnectOnboarding/StripeConnectOnboardingfor Express account KYC (refresh/return URLs). Refunds reverse the application fee by default. No Nest, nostripeSDK dependency — the consuming app passes a Stripe-shaped client in.packages/social→@bubltec/mycota-social—SocialPublisherport, a capability map that is honest about what each official API can actually do (TikTok is draft-only until partnership approval; Instagram Stories are not claimed), aSocialPublisherRegistryfor fan-out, plus Meta / TikTok / X adapters over injectedfetch. Connections aremeta|tiktok|x: Facebook Login for Business lists Pages + linked Instagram via/me/accounts; Instagram and Facebook Page are opted in separately. Instagram Login and Threads are separate OAuth products.insights()reads native Graph / X public metrics after publish.packages/media→@bubltec/mycota-media—MediaStoreport,FakeMediaStorefor local/tests,S3MediaStoreover an injected object-store client. Public objects use a CDN/base URL; private objects use signed GET. No AWS SDK dependency.packages/jobs→@bubltec/mycota-jobs—JobSchedulerport for delayed work (campaign beats days out).FakeJobScheduler.processDuelocally;EventBridgeJobScheduleruses Schedulerat()in production — SQS delay is 15 minutes and is the wrong primitive.packages/tokens→@bubltec/mycota-tokens—TokenVaultfor OAuth access/refresh tokens. Refreshes before expiry, marksneeds_reauthwhen refresh fails.EncryptedTokenStore+AesGcmSecretBoxat rest. Meta / TikTok / X refreshers over injectedfetch. Genericproviderstring — not coupled to@bubltec/mycota-social.packages/postgres→@bubltec/mycota-postgres—SqlDatabaseover an injectedpg.Pool(nopgdependency here). Nestedtransactionjoins the open transaction.applyMigrations+VECTOR_EXTENSION. Product tables stay in the consuming app. Local Docker / RDS are the same adapter — there is no in-memory SQL fake.packages/mail→@bubltec/mycota-mail—Mailerport,FakeMailerfor local/tests,SesMailerover an injected SES-shaped client. Non-prod stages prefix subjects with[stage]. No AWS SDK dependency.packages/sms→@bubltec/mycota-sms—SmsSenderport,FakeSmsSenderfor local/tests,SnsSmsSenderover an injected SNS-shaped client. Numbers are normalized to E.164. No AWS SDK dependency.
The core idea: real config lives in SSM Parameter Store under
/{namespace}/{env}/{key} (plus /{namespace}/shared/{key} for values every
environment shares), where env is any string, not a fixed 'dev' | 'prod' enum — pr-123, a branch name, whatever your CI generates for an
ephemeral preview stack, works with zero code changes.
import { loadSsmConfig, pushSsmConfig, cloneSsmNamespace, deleteSsmNamespace } from '@bubltec/mycota-config';
// Fetch everything under /myapp/dev/* + /myapp/shared/*, decrypted, keyed
// by leaf parameter name. Returns {} for an unprovisioned env — never
// throws — so a stack that hasn't been set up yet still boots.
const config = await loadSsmConfig({ namespace: 'myapp', env: 'dev' });
// Write local values up to SSM.
await pushSsmConfig({ namespace: 'myapp', env: 'dev' }, { 'jwt-secret': '...' });
// Ephemeral stack bootstrap: spin up "pr-123" pre-populated from "dev"'s values.
await cloneSsmNamespace({ namespace: 'myapp', sourceEnv: 'dev', targetEnv: 'pr-123' });
// Ephemeral stack teardown: call when the PR closes / the preview stack is destroyed.
await deleteSsmNamespace({ namespace: 'myapp', env: 'pr-123' });A CLI ships too (mycota-config), for local dev / CI scripts that would
rather shell out than import: mycota-config load --namespace myapp --env dev prints dotenv-format KEY=VALUE lines to stdout (pipe it to a file —
it's real secret values, not names); push/clone/delete mirror the
functions above. A project's own infra/scripts/secrets.ts-equivalent
becomes a thin wrapper around this instead of a from-scratch
implementation.
@bubltec/mycota-auth builds on this with buildMycotaAuthConfigFromSsm({ namespace, env, overrides? }), which maps a documented SSM key convention
(jwt-secret, web-origin, users-table-name, email-from-address,
session-cookie-name, stage, aws-region, bedrock-inference-profile-id,
brave-search-api-key, github-client-id/github-client-secret/
github-callback-url, google-* equivalents) onto MycotaAuthConfig —
so wiring a brand-new project's auth to SSM is one call:
MycotaAuthModule.forRootAsync({
useFactory: () => buildMycotaAuthConfigFromSsm({ namespace: 'myapp', env: process.env.STAGE! }),
});Ephemeral stacks — fully covered, runtime and infra. The four
primitives above are exactly what a PR-preview or throwaway test
environment needs: clone a template environment's config on creation, load
it at runtime (gracefully degraded if it hasn't been cloned yet), delete it
on teardown. @bubltec/mycota-cdk (below) closes the loop at the infra layer too —
an actual construct that calls these primitives from a Lambda-backed
CloudFormation custom resource, so an ephemeral stack's own CDK app can
provision and tear down its config without any project-specific glue code.
mycota is opinionated about CDK as its IaC layer — same TypeScript-first reasoning as the rest of the framework, no context-switch to a separate templating language, and it's what btfp itself already deploys with.
import {
grantSsmConfigRead,
EphemeralConfig,
MediaBucket,
JobQueue,
PostgresInstance,
SesDomain,
GithubActionsDeployRole,
} from '@bubltec/mycota-cdk';
// Generalized version of a single ssm.StringParameter...grantRead(handler)
// call — scope a Lambda/ECS role to read everything under a namespace/env
// (plus shared/ by default) in one line.
grantSsmConfigRead(myLambda, { namespace: 'myapp', env: 'dev' });
// Bootstrap + tear down an ephemeral stack's config from the stack's own
// CDK app: Create clones sourceEnv -> targetEnv, Delete removes targetEnv's
// parameters, Update is a deliberate no-op (never silently overwrite config
// someone hand-tweaked for this one ephemeral env after creation).
new EphemeralConfig(this, 'Config', {
namespace: 'myapp',
sourceEnv: 'dev',
targetEnv: 'pr-123',
});
// Private S3 + CloudFront OAC for posters / Reels / print PDFs.
const media = new MediaBucket(this, 'Media', { namespace: 'myapp', env: 'dev' });
media.grantReadWrite(myLambda);
// SQS worker + EventBridge Scheduler group. Campaign beats days out use
// Scheduler `at()`, not SQS DelaySeconds (15 min cap).
const jobs = new JobQueue(this, 'Jobs', { namespace: 'myapp', env: 'dev' });
jobs.grantSchedule(myLambda);
jobs.grantConsume(myWorker);
// RDS Postgres 16. The app brings the VPC. pgvector is a migration
// (`VECTOR_EXTENSION`), not a CloudFormation property.
const db = new PostgresInstance(this, 'Db', { namespace: 'myapp', env: 'dev', vpc });
db.allowDefaultPortFrom(myLambda);
db.grantSecretRead(myLambda);
// Verified SES domain. App brings the hosted zone (usually a product subdomain).
const email = new SesDomain(this, 'Email', { hostedZone });
email.grantSendEmail(myLambda);
// GitHub Actions OIDC role for `cdk deploy`. Trusts `main` plus the
// `development` and `production` GitHub Environments by default.
const ci = new GithubActionsDeployRole(this, 'Gha', {
repository: 'acme/myapp',
roleName: 'myapp-gha-deploy',
});aws-cdk-lib/constructs are peer dependencies, not bundled — a consuming
project supplies its own pinned CDK version, avoiding the "two copies of
constructs" jsii error that comes from a transitive dependency shipping its
own copy.
Published to the public npm registry under the @bubltec scope:
@bubltec/mycota-config, @bubltec/mycota-dynamo, @bubltec/mycota-auth,
@bubltec/mycota-professional-verification, @bubltec/mycota-cdk,
@bubltec/mycota-payments, @bubltec/mycota-social, @bubltec/mycota-media,
@bubltec/mycota-jobs, @bubltec/mycota-tokens, @bubltec/mycota-postgres, @bubltec/mycota-mail, @bubltec/mycota-sms. All packages version in
lockstep. The checked-in version field in each package.json is a permanent
placeholder (0.0.0) — it's never authoritative; CI computes the real version
fresh at publish time from git tags.
Every relevant push to main (.github/workflows/ci.yml) publishes to npm.
Real semver releases also get a vX.Y.Z git tag and a GitHub Release. Bump
level is chosen automatically:
feat:PR/commit → minor bump (e.g.0.1.0→0.2.0)fix:PR/commit → patch bump- everything else → patch bump
- override with
#major,#minor, or#patchin the PR title (squash-merge subject); use#nextto publish a rolling prerelease to thenextdist-tag only (no git tag, no GitHub Release)
To cut a release from current main without a new commit: Actions → CI → Run
workflow → choose the bump level.
Install whichever packages you need:
pnpm add @bubltec/mycota-auth @bubltec/mycota-config @bubltec/mycota-dynamo
# or, to track the rolling prerelease instead of the latest real release:
pnpm add @bubltec/mycota-auth@next @bubltec/mycota-config@next @bubltec/mycota-dynamo@nextBecause @bubltec/mycota-auth's internal deps (@bubltec/mycota-config,
@bubltec/mycota-dynamo) are ordinary published dependencies, installing
@bubltec/mycota-auth alone pulls them in transitively — no workspace
linking, no submodule, no manual build step required.
The framework itself is not bundled. @bubltec/mycota-auth,
@bubltec/mycota-dynamo and @bubltec/mycota-professional-verification declare
NestJS 12 (@nestjs/common, @nestjs/core, @nestjs/jwt, @nestjs/passport),
class-validator ^0.15, class-transformer, passport, reflect-metadata and rxjs
as peer dependencies, so your app owns exactly one copy. Two copies would break DI
and make class-validator decorators register into a store your ValidationPipe
never reads. pnpm and npm 7+ install peers automatically; if you pin your own
versions, keep them within the ranges above. Built and tested on Node 26 with
TypeScript 7 ("types": ["node"] must be set explicitly in your tsconfig under TS 7).
To pick up a newer build later, re-run pnpm add <pkg> (or <pkg>@next)
— npm/pnpm dependency versions are pinned at install time, not
auto-updating, so this is a deliberate "pull latest" action, not a
persistent floating reference.
No package here depends on anything outside this repo (the
@btfp/shared-types coupling this README used to flag has been removed —
User/AuthProvider are defined locally in @bubltec/mycota-auth now).
For a fast local edit-and-test loop against a consuming project without
waiting for a dev publish: pnpm link --global from each package directory
here, then pnpm link --global @bubltec/mycota-auth (etc.) in the
consuming project.
See apps/bff/src/mycota-config.ts in the btfp repo for a real (pre-SSM,
env-var-based) example of building MycotaAuthConfig by hand — worth
comparing against buildMycotaAuthConfigFromSsm above to see what the SSM
convention saves you from writing yourself.
pnpm install
pnpm turbo run typecheck lint build testRuns standalone — no btfp parent checkout needed.