English | 日本語
Flarelet is a serverless application platform for AWS. Define what your application needs in YAML, push your code, and Flarelet handles infrastructure, authentication, previews, deployment, and runtime bindings.
- Your app is an ordinary web app (ASGI for Python, Hono and friends for TypeScript), run on Lambda with the Lambda Web Adapter
- Authentication is on by default (Cognito plus Flarelet's own auth layer). Opt out explicitly with
auth: falsewhen you want a public app - Databases (DynamoDB), storage (S3), AI (Bedrock) and secrets are declared, not provisioned by hand. You never write IAM or CDK
- Git is first-class: one environment per branch, one preview environment per pull request (deleted automatically when the PR closes)
- CI is GitHub Actions with OIDC. No long-lived access keys
The mental model is just this:
flarelet.yaml
flarelet init
flarelet dev
flarelet deploy
flarelet logs
For the design, see docs/specs/FLARELET_V0_DESIGN.md. For implementation decisions, see docs/specs/DECISIONS.md.
Request flow at runtime (a dotted line marks an optional or opt-in path):
flowchart LR
client["Browser / client"] --> api
subgraph aws["Your AWS account"]
api["API Gateway HTTP API<br/>$default route, throttling, access logs"]
front["front auth Lambda<br/>(auth enabled)"]
app["app Lambda<br/>Lambda Web Adapter + your app"]
cognito["Cognito User Pool<br/>Managed Login, PKCE"]
secret["Secrets Manager<br/>cookie signing key"]
epoch["SSM Parameter Store<br/>session epoch"]
ddb[("DynamoDB")]
s3[("S3")]
bedrock["Amazon Bedrock"]
ssm["SSM SecureString<br/>secrets, loaded as env vars at start"]
logs["CloudWatch Logs<br/>app, front, API access logs"]
alarm["CloudWatch alarms<br/>Lambda, HTTP API, DynamoDB metrics"]
end
idp["External IdP<br/>Google / Entra ID / OIDC"]
sns["Your SNS topic"]
api -->|"auth enabled"| front
api -.->|"auth: false"| app
front -->|"invoke"| app
front <--> cognito
cognito <-.->|"optional"| idp
front --> secret
front --> epoch
app --> ddb
app --> s3
app --> bedrock
app --> ssm
api --> logs
front --> logs
app --> logs
alarm -.->|"opt-in: alerts"| sns
- Requests enter through one API Gateway HTTP API (
$defaultroute). Withauthenabled, the front auth Lambda signs users in through Cognito (Managed Login, PKCE; an external IdP is optional) and then invokes the app Lambda. Withauth: false, the API calls the app Lambda directly - The front auth Lambda reads only the cookie signing key (Secrets Manager) and the session epoch (SSM Parameter Store)
- The app Lambda runs your app with the Lambda Web Adapter, and reaches only the DynamoDB tables, S3 buckets, Bedrock models and secrets you declared
- CloudWatch alarms and the SNS notification exist only when you set
alerts
Stacks and deployment:
flowchart TB
dev["Developer machine<br/>flarelet CLI"]
gha["GitHub Actions"]
ci["CI role"]
boot["CDK bootstrap roles"]
cfn["CloudFormation"]
dev --> boot
gha -->|"OIDC"| ci
ci -->|"assume"| boot
boot --> cfn
subgraph persistent["Persistent stage, e.g. prod"]
direction LR
stage["stage stack<br/>flarelet-{app}-{stage}<br/>RETAIN<br/>Cognito User Pool, DynamoDB, S3,<br/>cookie signing key, session epoch<br/>S3 versioning, DynamoDB deletion protection"]
version["version stack<br/>flarelet-{app}-{stage}-{version}<br/>recreated per version<br/>HTTP API, Lambdas, App Client, log groups"]
version -->|"uses"| stage
end
subgraph preview["PR preview"]
pr["single stack<br/>flarelet-{app}-preview-pr-{N}<br/>destroyed when the PR is closed"]
end
cfn --> stage
cfn --> version
cfn --> pr
- A persistent stage is split in two. The stage stack holds the data and auth state (RETAIN). The version stack holds the HTTP API and Lambdas and is created anew per version
- A PR preview is a single stack that is destroyed when the PR closes. It has no Cognito User Pool
- Both the flarelet CLI and GitHub Actions (OIDC, no long-lived keys) deploy through the CDK bootstrap roles and CloudFormation
- Node.js 24
- AWS credentials (SSO is fine) for the target account. The region is resolved from
--region, thenAWS_REGION, thenAWS_DEFAULT_REGION(defaultus-east-1) - A CDK bootstrap (the
CDKToolkitstack) in the target account and region, once. The firstflarelet deployin a terminal does it for you; for CI, non-interactive runs or--no-bootstrap, runflarelet bootstrap aws --region <region>once beforehand (no globalcdkCLI is needed) - Docker, for Python apps (dependencies are bundled in a Lambda build container)
Runtimes and tasks of this repository are managed with mise.
Node.js 24 or later is all you need to run it.
npx flarelet@latest init # try it without installing
npm i -g flarelet # install globallygit clone https://github.com/youyo/flarelet && cd flarelet
mise install # Node 24
npm install
mise run build # build the CLI into dist/
node dist/cli/index.js --helpUse npm link (or an alias to the build output) to get a flarelet command.
To try local changes instead of the published npm package, build a tarball and install it. dist/ is not committed and there is no prepare script, so a plain git spec (npm install github:youyo/flarelet) does not produce a working CLI:
mise run build
npm pack # creates flarelet-0.1.0.tgz
npm install -g ./flarelet-0.1.0.tgz # or host the tarball and install it from its URLThe GitHub Actions workflow can install such a tarball too: set the repository variable FLARELET_PACKAGE to its URL (see GitHub Actions).
For zsh completion, see Shell completion (zsh).
flarelet init myapp --runtime python # or typescript
cd myapp
flarelet dev # run locally with hot reload (http://localhost:8787)
flarelet deploy # deploy to AWS and print the URL
flarelet auth user add me@example.com --stage prod # invite someone who can sign in
flarelet logs --follow # follow the app logsinit creates:
myapp/
├── flarelet.yaml
├── app/ # starter app
├── .github/workflows/flarelet.yml # GitHub Actions (an existing file is never overwritten)
└── .gitignore # adds .flarelet/
Where deploy goes is decided from Git. By default the default branch is prod/current and a pull request is preview/pr-<number>. To be explicit, use --stage prod --version v1. Only preview/pr-<number> is ephemeral; an explicit --stage preview --version pr-5 is treated as the same PR preview as --pr 5 (prod/pr-5 and the like are ordinary persistent versions).
Other useful commands:
flarelet validate # validate flarelet.yaml
flarelet plan # show what would be created or changed, in Flarelet's terms
flarelet secret set EXTERNAL_API_KEY # the value comes from stdin or a prompt (never from arguments)
flarelet destroy # delete this versionUnknown keys are errors. When in doubt, run flarelet validate.
version: 1 # required
name: myapp # required. lowercase letters, digits and hyphens; starts with a letter; 2-24 characters
runtime: # required
language: python # python | typescript
version: "3.13" # optional, a quoted string (python default 3.13, typescript default 24)
http:
true # authenticated. http: { auth: false } makes it public without authentication
# omit http for no HTTP endpoint
# For an external IdP / access restriction (see "Authentication"):
# auth:
# provider: google # google | oidc | entra (default: invite-only Cognito)
# allow: # default: anyone the IdP authenticates
# domains: [example.com]
# emails: [alice@example.com]
database: # DynamoDB. one table per name
main: {}
storage: # S3. one bucket per name
files: {}
ai: # Bedrock. logical names are resolved by Flarelet's model registry
models:
- sonnet # sonnet | opus | haiku | nova-micro | nova-lite | nova-pro
secrets: # set values with `flarelet secret set`; passed to the app as environment variables
- EXTERNAL_API_KEY
git: # optional. default: default branch -> prod/current, PR -> preview/pr-N
production:
branch: "release/*" # glob (* does not cross /, ** does) or default
version: branch # branch: derive the version from the branch name (wildcard part). A fixed name also works
preview:
branch: default # this branch deploys to preview/current
pullRequests: true # false: do not create PR previewshttp options in detail:
| Form | Meaning |
|---|---|
http: true |
Authenticated (default invite-only Cognito) |
http: false |
No HTTP endpoint (same as omitting http) |
http: { auth: true } |
Same as http: true |
http: { auth: false } |
Public, no authentication (persistent stages only; PR previews still force Preview Auth) |
http: { auth: { ... } } |
External IdP and/or access restriction (see Authentication) |
The HTTP API has a throttle by default, as a guardrail against runaway traffic and cost (denial of wallet). Persistent stages allow rate: 1000 req/s with burst: 2000; PR previews allow rate: 100 with burst: 200. Override or disable it in flarelet.yaml:
http:
throttle: { rate: 2000, burst: 4000 } # rate: positive number (<= 10000), burst: positive integer (<= 5000)
# throttle: false # no throttling (API Gateway account limits still apply)Each request also consumes Lambda concurrency twice (front + app), and the account default is 1000 concurrent executions, so raise the numbers only after checking your quotas. The API also writes a one-line JSON access log (request ID, source IP, method, route, path, status, latencies; no headers, query strings, cookies or tokens) to a log group in the version stack (retention 1 month, deleted with the stack).
Upgrading an existing deployment: the next
flarelet deployturns on S3 versioning (noncurrent versions expire after 30 days) and DynamoDB deletion protection for persistent stages, and applies the default throttle.flarelet destroy --stage-resourcesstill removes protected tables.
Alarms are opt-in. Without alerts: the generated templates are unchanged. Set an existing SNS topic and Flarelet creates the alarms below for persistent stages only (PR previews and flarelet dev never get alarms, even when alerts is set):
alerts:
topicArn: arn:aws:sns:ap-northeast-1:123456789012:ops-alerts # an existing standard (non-FIFO) topic in the deploy region| Alarm (5-minute period, 1 evaluation period; notifies on ALARM and OK) | Condition |
|---|---|
app Lambda and front Lambda (front only when auth is on): Errors |
Sum >= 5 |
app Lambda and front Lambda: Throttles |
Sum >= 1 |
HTTP API (when http is set): 5xx (AWS/ApiGateway, dimensions ApiId + Stage) |
Sum >= 5 |
each DynamoDB table: SystemErrors (summed over operations) |
Sum >= 1 |
each DynamoDB table: ReadThrottleEvents + WriteThrottleEvents |
Sum >= 1 |
- Missing data is treated as not breaching. The thresholds are fixed and cannot be changed in
flarelet.yaml - Flarelet does not create the SNS topic (so you control subscriptions, such as email confirmation, and the topic lifecycle). The topic must be in the same region as the deployment, otherwise
synth/deployfails - If the topic is encrypted with a customer managed KMS key, the key policy must allow CloudWatch to use it (
cloudwatch.amazonaws.comneedskms:Decryptandkms:GenerateDataKey*); the defaultalias/aws/snskey cannot be used with CloudWatch alarms - Alarm names are
<stack name>-<target>, for exampleflarelet-myapp-prod-v1-app-errorsandflarelet-myapp-prod-database-main-system-errors
| Declaration | Environment variable |
|---|---|
database.<name> |
FLARELET_DATABASE_<NAME>_TABLE |
storage.<name> |
FLARELET_STORAGE_<NAME>_BUCKET |
ai.models[] |
FLARELET_AI_<NAME>_MODEL_ID |
secrets[] |
the declared name, as is |
Secret names use A-Z, 0-9 and _, and names starting with FLARELET_ or AWS_ are reserved. Credentials of external IdPs are not declared in secrets: (see Authentication).
App conventions: Python needs app/main.py exposing app (ASGI) and app/requirements.txt. TypeScript needs app/index.ts and app/package.json and listens on PORT (port 8080). In TypeScript, listen with hostname: process.env.HOST ?? "127.0.0.1" (same as the flarelet init template). On Lambda, the Lambda Web Adapter reaches 127.0.0.1, and flarelet dev passes HOST=127.0.0.1. Listening on all interfaces would let other machines on the same network reach the app directly, bypassing the proxy, and forge identity headers.
A deployment is addressed by App -> Stage -> Version:
- App: the
nameinflarelet.yaml - Stage: a long-lived environment that owns the stateful resources (database, storage, users, secrets). Lowercase letters, digits and hyphens, up to 16 characters
- Version: one deployed copy of the code inside a stage. Up to 32 characters
Git decides the target unless you pass it explicitly. Priority: --stage + --version > --pr > --branch > the current Git state (the GitHub event in CI).
| Git state | Default target | Configurable with |
|---|---|---|
| Default branch | prod/current |
git.production.branch, .version |
| A branch matching a glob | prod/<version> |
git.production.branch: "release/*", version: branch (release/v1 -> prod/v1) |
git.preview.branch |
preview/current |
git.preview.branch |
| Pull request N | preview/pr-N |
git.pullRequests: false to disable |
- Only
preview/pr-<N>is ephemeral (the PR preview, destroyed when the PR closes). It starts with empty databases and storage; production data is never copied - Branches that match no mapping cannot be deployed until you pass
--stage/--version --stagealone derives the version from Git, or falls back tocurrent
With http: true, the default is invite-only Cognito (self sign-up is disabled). Only users invited with flarelet auth user add <email> --stage <stage> can sign in. Pull request previews are protected separately by Preview Auth (token links).
PR previews are protected by Preview Auth even with http.auth: false, so a PR's code is never published without authentication by accident (plan / deploy print forced for pull request previews). Only persistent stages such as prod are ever public with auth: false.
The session cookie is __Host-flarelet_session (Secure, Path=/, no Domain). The temporary sign-in cookie is __Secure-flarelet_flow. Sessions last 8 hours at most.
To stop sessions earlier, run flarelet auth revoke-sessions --stage <stage> (--pr <number> for a PR preview). This revokes all Flarelet sessions of that environment, and takes effect within 60 seconds because the front caches the session generation for 60 seconds. If the generation cannot be read, the front returns 503 rather than letting requests through unchecked. flarelet auth user remove performs the same revocation so a removed user's sessions stop (other users sign in again).
Authenticated users reach the app as the headers x-flarelet-user-sub, x-flarelet-user-email and x-flarelet-user-email-verified (true or false). Any x-flarelet-* header sent by the client is removed by the front auth layer. In TypeScript, identity(headers) from flarelet/runtime returns sub, email and emailVerified. On a stage with auth: false, the app receives FLARELET_AUTH_ENABLED=false and identity() always returns null (see Limitations).
Use sub for authorization and for linking users. email comes from the IdP and is not necessarily verified (with OIDC and others, an IdP may allow unverified addresses). If you use email, only trust it when emailVerified (the header is true). emailVerified is true only when the id_token has email_verified set to true (Entra ID does not emit email_verified, so it is always false; PR previews have no email, so it is false). The Cognito (default) User Pool keeps the original email until the new address is verified when a user changes it.
An external IdP is wired in as a Cognito identity provider, and users go straight to it, skipping the sign-in page. Client IDs and secrets have fixed names and are set per stage with flarelet secret set (do not list them in secrets:; they are not passed to the app). They are stored in Secrets Manager at flarelet/<app>/<stage>/auth/<name> and never enter the template.
| provider | flarelet.yaml | Credentials (flarelet secret set <name> --stage <stage>) |
|---|---|---|
auth: { provider: google } |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
|
| oidc | auth: { provider: oidc, issuer: https://idp.example.com, scopes?: [...], name?: Corp } |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
| entra | auth: { provider: entra, tenant: <directory (tenant) ID> } |
ENTRA_CLIENT_ID / ENTRA_CLIENT_SECRET |
- The redirect URI to register at the IdP is
https://<prefix>.auth.<region>.amazoncognito.com/oauth2/idpresponse. The prefix is derived deterministically from app, stage and AWS account. If you runflarelet deploybefore setting credentials, the error message shows the exact URI flarelet deploywithout credentials stops before creating anything and tells you whichflarelet secret set ...to run. After updating a secret, deploy again to apply it to the IdP- oidc:
issuermust be https.scopesdefaults toopenid email profile(openidis required).nameis the display name on the sign-in page (defaultOIDC) - entra:
tenantis the directory (tenant) ID, a GUID. Domain names (contoso.onmicrosoft.com) do not work, because Entra's token issuer uses the tenant ID form and Cognito's issuer check would fail. Multi-tenant and personal-account values such ascommon,organizationsandconsumersare rejected - Users invited with
flarelet auth user addcannot sign in on an external-IdP stage (a warning is printed) - PR previews do not use the external IdP; they stay on Preview Auth
-
In Google Cloud Console, open "APIs & Services -> OAuth consent screen" and set User type to Internal. Only users in your Google Workspace organization can then sign in (recommended)
-
Under "Credentials -> Create OAuth client ID", choose "Web application" and register the
/oauth2/idpresponseURI above as an authorized redirect URI -
Register the client ID and secret, then deploy
flarelet secret set GOOGLE_CLIENT_ID --stage prod flarelet secret set GOOGLE_CLIENT_SECRET --stage prod flarelet deploy
-
For defense in depth, also put your Workspace domain in
allow.domains(next section). Even if Internal is misconfigured as External, accounts outside your organization are rejected on the Flarelet side
-
In the Microsoft Entra admin center, go to "App registrations -> New registration" and choose "Accounts in this organizational directory only (single tenant)" as the supported account type
-
Register
https://<prefix>.auth.<region>.amazoncognito.com/oauth2/idpresponseas a redirect URI (platform "Web") -
Under "Certificates & secrets -> New client secret", create a secret and note its value (not the secret ID). Rotate it before it expires, then
flarelet secret setand deploy again -
Under "API permissions", confirm the delegated Microsoft Graph permissions
openid,profileandemail -
Use "Application (client) ID" and "Directory (tenant) ID" from the overview page
http: auth: provider: entra tenant: 00000000-0000-0000-0000-000000000000 # directory (tenant) ID allow: { domains: [contoso.com] } # optional
flarelet secret set ENTRA_CLIENT_ID --stage prod flarelet secret set ENTRA_CLIENT_SECRET --stage prod flarelet deploy
- The User Pool requires the email attribute, so Entra users without an email address (
emailclaim) cannot sign in - Guest (B2B) users invited to your tenant can also pass the tenant-specific sign-in. To keep guests out, set your own domain in
allow.domains
Narrows down who gets into the app among the users the IdP authenticated (default: everyone the IdP authenticates). It works with every provider, including the default Cognito.
http:
auth:
provider: google
allow:
domains: [example.com] # either one matching is enough
emails: [partner@gmail.com]| provider | domains |
emails |
|---|---|---|
The Google Workspace hosted domain (hd) matches |
The email_verified=true email matches exactly |
|
| cognito / oidc | The domain of the email_verified=true email matches exactly (subdomains are different) |
The email_verified=true email matches exactly |
| entra | The domain of the email (or preferred_username if none) matches exactly |
The email (or preferred_username) matches |
- google does not decide by email domain, so that personal Google accounts created with a company-domain address (outside Workspace, no
hd) are rejected - Entra id_tokens have no
email_verified, so the check is by email and relies on the tenant-specific sign-in (accounts outside the tenant cannot get in) - The check happens at sign-in (callback) in the front auth Lambda. A rejected user gets a 403 page (with a sign-out link) and no session cookie
- When you change
allowand deploy, sessions issued before are invalidated and the check runs again at the next sign-in - Comparison is case-insensitive. The lists are passed through an environment variable of the front Lambda, so the total is capped at 2000 characters (use
domainsif you have many) allowdoes not apply to PR previews (Preview Auth)
flarelet dev # http://localhost:8787
flarelet dev --as alice@example.com # run as a signed-in user
flarelet dev --stage prod --version v1 # connect to an existing environment's DB/storage (no new stack)- By default it uses a dedicated dev environment,
preview/local-<OS user name>. Ifdatabase:/storage:are declared, it creates a stack with only those on first use (reused afterwards) and passes them to the local app in the same environment variables as production (FLARELET_DATABASE_*and so on). With neither, no stack is created - AI model IDs are resolved through the model registry, and Bedrock is called with your local AWS credentials.
secrets:are decrypted from the stage and put in environment variables (values are never printed) - Changes under
app/restart the app (node_modules,__pycache__,venv, dotfiles and the like are ignored). On a build failure or crash it waits for the next change - Python runs as
python -m uvicorn main:app(pythonon PATH, falling back topython3) withPYTHONDONTWRITEBYTECODE=1, to avoid a stale.pycbeing used when a file is rewritten in the same second with the same size. Installfastapi,uvicornand your other dependencies in your own environment (a venv is recommended). TypeScript is bundled with the same esbuild as production (no extra dependencies) - A thin proxy listens on the user-facing port (127.0.0.1) and always strips
x-flarelet-*headers sent by the client. Only with--as <email>does it addx-flarelet-user-email,x-flarelet-user-email-verified: true,x-flarelet-user-sub: dev:<email>andx-flarelet-auth-mode: dev(locally there is no sign-in page and noallowcheck). Because a layer strips headers, the app receivesFLARELET_AUTH_ENABLED=true - The proxy returns 403 for any
Hostother thanlocalhost,127.0.0.1or[::1](with an optional port), so external sites cannot read your local app through DNS rebinding. With--asit also returns 403 for cross-site requests (Originis not the app's own origin, orSec-Fetch-Site: cross-site), which prevents CSRF as the pseudo user - The app receives
HOST=127.0.0.1and a random per-launchFLARELET_DEV_SECRET, and the proxy adds the same value as thex-flarelet-dev-secretheader only to requests it forwards.identity()inflarelet/runtimereturns an identity only when the header matchesFLARELET_DEV_SECRET(so forged headers on direct access that bypasses the proxy are not trusted even if the app listens on all interfaces). Python has no runtime library, so when you read identity headers, trust them only ifx-flarelet-dev-secretmatchesFLARELET_DEV_SECRETwhenever that variable is set (uvicorn is started with--host 127.0.0.1) FLARELET_OFFLINE=1starts only the app, without connecting to AWS (bindings show asoffline)- To delete the dev environment:
flarelet destroy --stage preview --version local-<user>
PR previews, branch deploys and cleanup run from GitHub Actions with OIDC.
-
Once per repository, create the role with admin credentials for the AWS account. CI never bootstraps automatically, so bootstrap the account and region first (once; it does nothing if already done).
bootstrap githubstops with a hint if it is missing.flarelet bootstrap aws --region ap-northeast-1 flarelet bootstrap github --repo owner/name --region ap-northeast-1
- If the account already has a GitHub OIDC provider, it is reused as is (never modified or deleted). Flarelet creates one only when there is none
- Only OIDC tokens for
repo:owner/name:*can assume the created role (see "Risk of the trust policy" below)
-
Set the repository variables printed after the command:
gh variable set FLARELET_AWS_ROLE_ARN --repo owner/name --body arn:aws:iam::123456789012:role/flarelet-github-owner-name gh variable set FLARELET_AWS_REGION --repo owner/name --body ap-northeast-1 gh variable set FLARELET_PACKAGE --repo owner/name --body https://example.com/flarelet-0.1.0.tgz
The workflow installs the exact Flarelet version that ran
flarelet init/flarelet workflow generate(npx flarelet@<version>).FLARELET_PACKAGEis optional: set it only to override the install source, such as a tarball URL or a git spec. -
Commit and push the
.github/workflows/flarelet.ymlthatflarelet initcreated.
| Event | Action |
|---|---|
| push (persistent branches) | flarelet deploy --ci |
| PR opened / synchronize / reopened | Deploy the preview. Comment the URL on the PR (one comment, kept up to date) and create a GitHub Deployment |
| PR closed | flarelet destroy --ci deletes the preview. The comment becomes "deleted" and the Deployment inactive |
- The push trigger (
on.push.branches) is generated from thegitsettings inflarelet.yaml:git.production.branchandgit.preview.branch, withdefaultreplaced by the default branch name. Glob patterns (*,**) keep their meaning - After changing the
gitsettings, regenerate the workflow withflarelet workflow generate --force. Without--forceit creates a missing file, reportsup to date, and refuses to change a file that differs (exit code 1) flarelet validateprints a warning when the existing workflow'son.push.branchesdiffers from thegitsettings- Manual edits to the workflow are lost with
--force. Keep custom changes in a separate workflow file - Runs for the same PR (branch) are serialized with
concurrency - Actions in the generated workflow are pinned to commit SHAs (with
persist-credentials: falseon checkout). To bring an existing generated workflow up to date, regenerate it withflarelet workflow generate --force - PRs from forks are not handled, because OIDC and secrets are unavailable to them
Previews are protected by Preview Auth. The PR comment contains only the URL and a hint to get a token link with flarelet env url --pr <number> --with-token, not the link itself (on a public repository anyone could open it). For private repositories only, adding flarelet github comment --with-token to the workflow puts the link in the comment (it is refused on public repositories).
sts:AssumeRoleon the CDK bootstrap roles (cdk-<qualifier>-{deploy,file-publishing,image-publishing,lookup}-role-*), and PassRole of thecfn-execrole (to CloudFormation only)- CloudFormation read access (Describe / Get / List), and
DeleteStackonly for PR preview stacks (flarelet-*-preview-pr-*;flarelet-bootstrap-*is explicitly denied) - SSM
GetParameteronly on the CDK bootstrap version (/cdk-bootstrap/<qualifier>/version; no/flarelet/*parameters) - Secrets Manager
DescribeSecret(onlyflarelet/*/auth/*, the external IdP credentials; deploy uses it to check presence without reading values) - Secrets Manager
GetSecretValue(only secrets taggedflarelet:stage=previewandflarelet:lifecycle=ephemeral, which are PR previews'. The cookie signing key of a persistent stage cannot be read) - CloudWatch Logs read access (
FilterLogEvents/GetLogEvents/StartLiveTail, only on Flarelet's log groupsflarelet-*)
It does not include writing secrets, deleting persistent stages or versions, full stage-resource deletion (--stage-resources) or user management. Run those with your own credentials.
The role's trust condition is token.actions.githubusercontent.com:sub = repo:owner/name:*. That means any workflow on any branch in that repository (including a modified workflow on a branch created by someone with push access) can use the role. The role can assume CDK bootstrap deploy roles, so in effect it can deploy arbitrary CloudFormation to that account and region. That is strong.
v0 leaves it as is. To narrow it, change the sub condition in the role's trust policy (in the IAM console, for example) after flarelet bootstrap github. Note that re-running the command reverts the change.
- Use a GitHub Environment: add
environment: productionto the workflow job, configure the Environment's protection rules (required reviewers, allowed deployment branches), and setsubtorepo:owner/name:environment:production - Limit to the branch and PRs: allow only
repo:owner/name:ref:refs/heads/mainandrepo:owner/name:pull_request(make theStringLikevalue an array) - Use separate roles for production deploys and PR previews, each with the conditions above
Delete the role with flarelet bootstrap github --repo owner/name --destroy. If Flarelet created the OIDC provider, it is deleted too, but only when no other role trusts it.
| Command | Description |
|---|---|
flarelet init [dir] [--runtime python|typescript] |
Create flarelet.yaml, a starter app and the GitHub Actions workflow |
flarelet validate |
Validate flarelet.yaml (also warns when the workflow's push branches differ from the git settings) |
flarelet workflow generate [--force] |
Generate .github/workflows/flarelet.yml from the git settings. --force overwrites a differing file |
flarelet synth |
Generate the CDK Cloud Assembly into .flarelet/out/ (for debugging) |
flarelet plan |
Show changes compared with what is deployed |
flarelet deploy [--ci] [--bootstrap] |
Deploy and print the URL. Bootstraps the account/region first if needed (in a terminal, or with --bootstrap; never with --ci or --no-bootstrap) |
flarelet destroy [--stage-resources --yes] [--ci] |
Delete a version. --stage-resources also fully deletes the stage's DB, storage, users and secrets |
flarelet env list |
List deployed stages and versions |
flarelet env url [--with-token] |
Print the URL. For a PR preview, --with-token gives a link with the token |
flarelet logs [--since 10m] [--follow] |
Show logs. --follow uses CloudWatch Logs Live Tail |
flarelet dev [--port 8787] [--as <email>] |
Run locally with hot reload, connected to the dev DB/storage on AWS (bootstraps like deploy; --bootstrap / --no-bootstrap) |
flarelet secret set|list|delete |
Manage secrets (SSM SecureString; external IdP credentials live in Secrets Manager) |
flarelet auth user add|list|remove <email> |
Manage Cognito users. remove also revokes all sessions of that stage |
flarelet auth revoke-sessions |
Revoke every Flarelet session of the environment (effective within 60 seconds) |
flarelet github comment [--state ...] [--with-token] |
Update the PR comment and the GitHub Deployment (for CI) |
flarelet bootstrap aws [--region <region>] [--qualifier <q>] |
CDK-bootstrap the account and region once (does nothing if already bootstrapped; never updates an existing CDKToolkit) |
flarelet bootstrap github --repo owner/name [--destroy] |
Create or delete the AWS IAM role for GitHub Actions (requires the account/region to be bootstrapped) |
flarelet completion zsh |
Print the zsh completion script (see Shell completion (zsh)) |
flarelet skill install [--global] [--dir <path>] [--force] |
Install the AI agent skill (see Agent skill) |
Common options that choose the deployment target: -f/--file, --stage, --version, --branch, --pr, --default-branch, --region.
flarelet completion zsh > "${fpath[1]}/_flarelet" # put it in the first fpath directory (effective from the next shell)
# or, in ~/.zshrc (after compinit)
eval "$(flarelet completion zsh)"The script is generated from the CLI's command definitions, so it follows new commands and options automatically. It completes commands, subcommands and options, plus the values of --runtime and --state and the paths of -f/--file and --dir (--stage / --version are not completed because that would call AWS). Only zsh is supported.
Flarelet bundles an Agent Skill so that AI agents such as Claude Code can use the flarelet CLI safely. It contains the workflows, safety rules (confirm destroy --stage-resources, pass secrets via stdin, use auth: false only when explicitly told, and so on), troubleshooting, and a reference of every command and every flarelet.yaml key.
There are two ways to install it:
# 1. With the skills CLI (works across agents; installing the CLI is not needed)
npx skills add youyo/flarelet # into this project
npx skills add youyo/flarelet -a claude-code -a codex # choose the target agents
npx skills add youyo/flarelet -g # globally
npx skills update # update installed skills
# 2. From the flarelet CLI (the skill bundled with the CLI you have installed)
flarelet skill install # into this project
flarelet skill install --global # into ~/ (available in every project)Use npx skills add to set it up for several agents or to follow the latest skill from GitHub. Use flarelet skill install to get the skill that matches the exact version of the CLI you run (the rest of this section describes this command).
- The real files are in
<root>/.agents/skills/flarelet/(SKILL.mdandreferences/).<root>/.claude/skills/flareletis a relative symlink to it (../../.agents/skills/flarelet).<root>is the current directory by default, another project root with--dir <path>, or your home directory with--global - An existing installation is not overwritten without
--force. If the same link already exists, nothing happens - Where symlinks cannot be created (Windows, for example), it prints a warning and copies to
.claude/skills/flarelet - In this repository,
.agents/skills/flarelet/is the source of truth and.claude/skills/flareletis the link. The skill text is checked against the CLI implementation by tests (commands and options exist, no broken links,flarelet.yamlexamples validate)
- Responses are limited to 6MB (the limit of a synchronous Lambda invoke). Go through S3 for large files
- Request timeouts are 30 seconds at most (app < front <= 30s). An app timeout becomes a 504
- With authentication, the path is HTTP API
$defaultroute -> front auth Lambda -> app Lambda, two stages - Logs are delivered with some delay.
flarelet logs --followuses CloudWatch Logs Live Tail, but ingestion into CloudWatch is not instantaneous - Cookies are scoped per host. Sessions are not shared across different hosts (different environments), so each environment signs in separately
- PR previews start with empty databases and storage; production data is not copied
- Verification of PR behavior on a real GitHub repository is out of scope for v0 (templates, resolver and CLI only)
--cidoes not handle fork PRs.destroy --cionly acts on closed PR events- Custom domains are out of scope for v0
- On a stage with
http.auth: false, nothing stripsx-flarelet-*headers. A client can freely sendx-flarelet-user-suband the like, so the app must not trust these headers.identity()fromflarelet/runtimelooks atFLARELET_AUTH_ENABLED=falseand always returnsnull, but when you read the headers directly (in Python, for example), use them only whenFLARELET_AUTH_ENABLEDistrue
mise run build # build into dist/
mise run test # unit + E2E (does not connect to AWS)
mise run test:unit # vitest (test/unit)
mise run test:e2e # run the built CLI as a real process (test/e2e, AWS side is FLARELET_OFFLINE=1)
mise run lint # ESLint + Prettier
mise run typecheck # tsc
mise run fmt # format with Prettier
mise run dev # tsc --watch
mise run pack:check # check the npm package contents (npm pack --dry-run)
mise run lint:actions # validate GitHub Actions workflows with actionlintOther tasks: test:e2e:file and test:e2e:aws:file (run a single file), test:e2e:python-env (venv for the flarelet dev E2E), playwright:install. Run mise tasks for the full list.
-
Always-on E2E (
mise run test:e2e): behavior that can be checked without connecting to AWS, such asinit,synth,planand the--cidecisions. It passes invalid credentials so that nothing can touch real AWS by accident -
Real AWS E2E (opt-in): actually deploys, checks HTTP, authentication and logs, then destroys. It creates only resources with test-specific names (
fe2e-*,flarelet-e2e/*) and verifies that nothing is left behind at the endFLARELET_E2E_AWS=1 mise run test:e2e:aws
It is skipped unless
FLARELET_E2E_AWSis set. It needs AWS credentials andAWS_REGION(ap-northeast-1). Thebootstrap githubE2E also verifies that it does not modify or delete a GitHub OIDC provider that already exists in the account. Thebootstrap awsE2E usesus-west-2, which must not be bootstrapped beforehand (it aborts otherwise): it checks thatdeploystops, thatdeploy --bootstrapbootstraps and deploys, and then deletes theCDKToolkitstack, its staging bucket and ECR repository.
.github/workflows/ci.yml runs on pull requests and pushes to main. It installs zsh (the completion E2E uses it), sets up tools with mise (Node, Python, actionlint), then runs mise run install, lint, lint:actions, typecheck, test (unit + always-on E2E, with no AWS credentials) and pack:check.
The development method is TDD (Red -> Green -> Refactor, with both unit and E2E tests).
For maintainers. Publishing is done by .github/workflows/release.yml when a v* tag is pushed. It uses npm trusted publishing (OIDC), so no npm token is stored anywhere, and provenance is attached automatically (public repository only).
- Bump
versioninpackage.json, commit, thengit tag vX.Y.Z && git push origin vX.Y.Z - The workflow runs lint / typecheck / test / pack:check, checks that the tag matches the
package.jsonversion, runsnpm publish, and creates a GitHub Release with generated notes
A trusted publisher can only be registered for a package that already exists on npm, so publish the very first version from your machine:
mise run install && mise run test # sanity check
npm login
npm publish --provenance=false # provenance cannot be generated locallyThen, on npmjs.com, open the flarelet package, go to Settings -> Trusted Publisher, choose GitHub Actions and register organization or user youyo, repository flarelet, workflow filename release.yml (with npm 11.15 or later, npm trust github flarelet --file release.yml --repo youyo/flarelet --allow-publish does the same). From then on, pushing a tag is all it takes. If you push the tag of the version you published by hand, the workflow skips the npm publish and only creates the GitHub Release.