From zero to your own Naute, in thirty minutes.
Naute ships as a single SAM template plus a small deploy script. Bring an AWS account, a domain you control, and a terminal. Run one script — and you'll have a markdown notebook running on your domain, in your account — idle for nothing, and a single writer's daily use comfortably fits inside the AWS free tier.
Six things to have ready.
None of these are unusual. If you've shipped anything on AWS before, you already have most of them.
An AWS account
With permissions to create Lambda, DynamoDB, Cognito, CloudFront, S3, ACM, and Route 53 resources.
A domain in Route 53
Naute provisions an ACM cert and a CloudFront alias. The hosted zone must live in the same account.
The AWS CLI, configured
Credentials with deploy rights, default region set to
us-east-1 (required by
CloudFront for the ACM cert).
The SAM CLI
Used to build and deploy the stack. Version 1.100 or newer is fine.
Node.js 22 & npm
Matches the Lambda runtime. The frontend and shared types build under the same toolchain.
An Anthropic API key
Optional, but needed if you want the AI features. Stored in SSM after the first deploy — see step 7.
You don't need the API key to deploy. You can stand up the stack first, then add the key when you're ready to turn AI on.
Fork, clone, build shared types.
Fork the repo first, then clone your fork. Cloning the upstream directly works for a local-only deploy, but step 9 (push-to-main CI/CD) needs you to own the remote — pushes have to land somewhere you control.
Naute is a monorepo with three workspaces —
shared,
backend, and
frontend — plus an
infra directory holding the SAM
template. The shared package has
to be built first because the other two import its types.
After this, npm run lint and
sam validate --lint -t infra/template.yaml
should both pass — a quick way to confirm your toolchain is wired
up before you spend any AWS credits.
Three environment variables. That's the lot.
The deploy script reads exactly three variables. Everything else — bucket names, Lambda configuration, DynamoDB keys — is derived inside the SAM template.
-
NAUTE_DOMAIN — the domain Naute will live on.
Example:
notes.example.com. - NAUTE_HOSTED_ZONE_ID — the Route 53 hosted zone ID that owns that domain.
- NAUTE_COGNITO_PREFIX — a unique prefix for your Cognito hosted UI domain. Must be globally unique across AWS.
Put these in your shell profile, a
direnv .envrc, or
wherever you keep per-project env vars. The deploy script refuses
to run if any of the three is missing.
First pass — provision the AWS resources.
./infra/deploy.sh builds shared
types, builds the SAM application, deploys the stack, then builds
and syncs the frontend. On this first run only the three
NAUTE_* vars from step 3 need to
be set — the frontend build inside will produce an incomplete
bundle (Cognito values don't exist yet), and we'll replace it in
step 5.
First runs take a little longer because ACM has to issue a certificate and CloudFront has to propagate. Don't open the app yet — the frontend won't be able to log you in until step 5.
Second pass — set the VITE_* values, redeploy.
Six VITE_* values get baked into
the frontend bundle at build time. The deploy script reads one of
them (VITE_GENERATE_URL, the
Function URL of the streaming AI Lambda) straight from the stack
outputs, so you never set it by hand. The other five are on you:
two (VITE_COGNITO_CLIENT_ID,
VITE_COGNITO_DOMAIN) come from
the stack you just created; the remaining three are fixed once
you've chosen a domain.
Now redeploy:
From here on every deploy is a single pass — SAM sees no infrastructure change and the script just re-syncs the (now correctly-built) frontend.
Prefer not to re-export those vars every time? Drop them into
frontend/.env.production
and Vite will pick them up automatically on every build. The five
values stay constant for the life of the stack.
From the same stack outputs you can also grab the production API
host, which step 8 needs to proxy requests from
npm run dev:
No sign-up — admin-create yourself.
Naute's Cognito pool is configured with
AllowAdminCreateUserOnly: true,
so there's no public sign-up. The first user — and every user
after — is created via the AWS CLI.
Pull the User Pool ID from the stack outputs:
Then create yourself with a temporary password:
Visit https://your-domain and
sign in with that email and temporary password. Cognito's hosted
UI prompts you to set a permanent password and — if you enabled
TOTP — register an authenticator app. After that, you're in.
One key in SSM. That's the whole switch.
Naute's AI features (generate, format) live behind a streaming Lambda that reads its Anthropic key from SSM Parameter Store. The path is hard-coded in the backend:
The Lambda caches it in memory, so a fresh value is picked up on the next cold start (or you can force a redeploy). Skip this step if you'd rather use Naute purely as a markdown editor — every other feature works without it.
Work on the frontend without redeploying.
Once the stack is up, you have everything you need to run the frontend locally against your real backend. Copy the example env file and fill in the values from the stack you just deployed:
The Cognito client is preconfigured to accept
http://localhost:5173/callback
as a callback URL, so OAuth works out of the box. For API calls, set
API_PROXY_TARGET in
.env.local to your deployed API
host (the ApiUrl output from step
5, e.g. https://api.your-domain)
— Vite uses it to proxy
/api
through to your real API Gateway. Without it, requests stay on
localhost and 404.
To push your changes live, run
./infra/deploy.sh again — SAM
will skip the no-op infrastructure change and just re-sync the
frontend.
Push to main, ship to AWS.
The repo ships a GitHub Actions workflow at
.github/workflows/deploy.yml
that runs ./infra/deploy.sh on
every push to main. It authenticates to AWS via OIDC — no
long-lived access keys. Everything below happens in
your fork, not the upstream — that's what owning the
remote bought you in step 2.
You'll need an IAM role in your AWS account that trusts
token.actions.githubusercontent.com
and is allowed to assume the same permissions the deploy script
uses locally. GitHub publishes the
exact trust-policy template
for this — paste it in, scope it to your repo, attach a deploy
policy, and copy the role ARN.
Then add the following secrets to the repository, under Settings → Secrets and variables → Actions:
AWS_ROLE_ARN
The ARN of the OIDC-trusted IAM role the workflow assumes.
Example:
arn:aws:iam::123…:role/naute-deploy.
NAUTE_DOMAIN
Same value as your local shell — the domain Naute is served from.
NAUTE_HOSTED_ZONE_ID
The Route 53 hosted zone ID that owns the domain above.
NAUTE_COGNITO_PREFIX
The globally-unique prefix used for the Cognito hosted UI domain.
VITE_API_URL
The API base URL the built frontend talks to — usually
/api
if you front it through CloudFront.
VITE_COGNITO_DOMAIN
The Cognito hosted UI domain, e.g.
https://my-naute.auth.us-east-1.amazoncognito.com.
VITE_COGNITO_CLIENT_ID
The Cognito app client ID exposed as a stack output after the first deploy.
VITE_REDIRECT_URI
Where Cognito sends users after login, normally
https://your-domain/callback.
VITE_LOGOUT_URI
Where users land after signing out, normally
https://your-domain.
The VITE_* values are baked into
the frontend bundle at build time, so changing one means
re-running the workflow (or
./infra/deploy.sh) to produce a
fresh build.
Once the secrets are in place, push to main and the
workflow takes over. Subsequent deploys are a
git push.
That's it. It's yours now.
No accounts to keep, no subscription to renew, no third-party to keep online for you. Read the source, change what you don't like, and ship it back if you make it better.