§ Self-host guide — ~30 minutes, end to end

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.

01 Before you start

Six things to have ready.

None of these are unusual. If you've shipped anything on AWS before, you already have most of them.

01

An AWS account

With permissions to create Lambda, DynamoDB, Cognito, CloudFront, S3, ACM, and Route 53 resources.

02

A domain in Route 53

Naute provisions an ACM cert and a CloudFront alias. The hosted zone must live in the same account.

03

The AWS CLI, configured

Credentials with deploy rights, default region set to us-east-1 (required by CloudFront for the ACM cert).

04

The SAM CLI

Used to build and deploy the stack. Version 1.100 or newer is fine.

05

Node.js 22 & npm

Matches the Lambda runtime. The frontend and shared types build under the same toolchain.

06

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.

02 Get the code

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.

# fork on github, then clone your fork $ git clone https://github.com/your-handle/naute.git $ cd naute # install all workspaces $ npm install # shared types must be built first $ npm run build -w shared

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.

03 Configure

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.
export NAUTE_DOMAIN=notes.example.com export NAUTE_HOSTED_ZONE_ID=Z0123456ABCDEF export NAUTE_COGNITO_PREFIX=my-naute

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.

04 Create the stack

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.

$ ./infra/deploy.sh ==> Building shared types ==> Building SAM application ==> Deploying SAM stack ==> Building frontend (incomplete — Cognito vars unset) ==> Syncing frontend to s3://... ==> Done

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.

05 Wire the frontend

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.

export VITE_API_URL=/api export VITE_REDIRECT_URI=https://$NAUTE_DOMAIN/callback export VITE_LOGOUT_URI=https://$NAUTE_DOMAIN export VITE_COGNITO_CLIENT_ID=$(aws cloudformation describe-stacks \ --stack-name naute \ --query "Stacks[0].Outputs[?OutputKey=='UserPoolClientId'].OutputValue" \ --output text) export VITE_COGNITO_DOMAIN=https://$NAUTE_COGNITO_PREFIX.auth.us-east-1.amazoncognito.com

Now redeploy:

$ ./infra/deploy.sh ==> Done (this time with a working frontend)

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:

$ aws cloudformation describe-stacks \ --stack-name naute \ --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \ --output text https://api.notes.example.com
06 Create your first user

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:

export USER_POOL_ID=$(aws cloudformation describe-stacks \ --stack-name naute \ --query "Stacks[0].Outputs[?OutputKey=='UserPoolId'].OutputValue" \ --output text)

Then create yourself with a temporary password:

$ aws cognito-idp admin-create-user \ --user-pool-id "$USER_POOL_ID" \ --username you@example.com \ --user-attributes Name=email,Value=you@example.com Name=email_verified,Value=true \ --temporary-password 'TempPass!1'

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.

07 Enable AI Optional

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:

$ aws ssm put-parameter \ --name /naute/anthropic-api-key \ --type SecureString \ --value "sk-ant-..."

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.

08 Local development Optional

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:

$ cp frontend/.env.example frontend/.env.local $ $EDITOR frontend/.env.local $ npm run dev -w frontend ➜ Local: http://localhost:5173

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.

09 Continuous deployment Optional

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

AWS_ROLE_ARN

The ARN of the OIDC-trusted IAM role the workflow assumes. Example: arn:aws:iam::123…:role/naute-deploy.

infra

NAUTE_DOMAIN

Same value as your local shell — the domain Naute is served from.

infra

NAUTE_HOSTED_ZONE_ID

The Route 53 hosted zone ID that owns the domain above.

infra

NAUTE_COGNITO_PREFIX

The globally-unique prefix used for the Cognito hosted UI domain.

frontend

VITE_API_URL

The API base URL the built frontend talks to — usually /api if you front it through CloudFront.

frontend

VITE_COGNITO_DOMAIN

The Cognito hosted UI domain, e.g. https://my-naute.auth.us-east-1.amazoncognito.com.

frontend

VITE_COGNITO_CLIENT_ID

The Cognito app client ID exposed as a stack output after the first deploy.

frontend

VITE_REDIRECT_URI

Where Cognito sends users after login, normally https://your-domain/callback.

frontend

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.