Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
# cloud-api backend (Go service)
# Default is the production endpoint; override if running cloud-api locally.
CLOUD_API_URL=https://cloud-api.nan.builders
# Server-side only. Required for /api/waitlist to write to the backend.
CLOUD_API_WAITLIST_KEY=

# Resend (transactional email)
# Server-side only. Optional — if either RESEND_API_KEY or RESEND_FROM_EMAIL
# is missing, /api/waitlist still succeeds but skips the confirmation email.
RESEND_API_KEY=
RESEND_FROM_EMAIL=no-reply@nan.builders
# Public signup (/api/signup) proxies to ${CLOUD_API_URL}/api/auth/signup.
# No API key: the endpoint is public and allowlists the nan.builders Origin.

# Per-key rate limits shown in the docs and served to the Discord bot.
# Not secrets: the deployed values live in wrangler.jsonc under `vars`, and
Expand Down
220 changes: 83 additions & 137 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

51 changes: 16 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
# NaN Website

Marketing site and public documentation for [nan.builders](https://nan.builders). Built with Astro 6 (SSR mode) on Cloudflare Workers, Preact islands for interactive components, Tailwind CSS v4, and an ES/EN i18n catalog. The site backs into the `cloud-api` Go service for waitlist registration and community signup, and uses Resend for transactional email.
Marketing site and public documentation for [nan.builders](https://nan.builders). Built with Astro 6 (SSR mode) on Cloudflare Workers, vanilla scripts (and a few Preact islands on the events pages), Tailwind CSS v4, and an EN/ES i18n catalog. Signup is open: the home form creates a free NaN account through the `cloud-api` Go service, which sends the sign-in email; every product unlocks with a plan on cloud.nan.builders.

## Tech stack

- Astro 6 (`output: 'server'`, Cloudflare adapter)
- Preact 10 for client-side islands
- Preact 10 for the few client-side islands (events)
- TypeScript (strict)
- Tailwind CSS v4 (Vite plugin)
- `rehype-pretty-code` + Shiki for code highlighting in docs
- `astro-i18n` plus a local helper (`src/lib/i18n.ts`) for ES/EN strings
- Vitest 4 for unit tests
- Cloudflare Workers (assets binding + SSR entrypoint)
- Resend for transactional email
- Node.js `>=22.12.0`, npm (lockfile is `package-lock.json`)

## Architecture

The site is a single Astro app deployed as a Cloudflare Worker. The landing (`/`), the community page (`/community`), and the docs (`/docs/*`) are SSR-rendered at the edge. Two API routes (`/api/waitlist`, `/api/community-signup`) validate input, forward to the `cloud-api` backend, and trigger Resend confirmation email. Interactive bits (waitlist form, community signup form, FAQ accordion) are Preact islands hydrated with `client:load` / `client:visible`. Static assets are served from the `ASSETS` binding configured in `wrangler.jsonc`.
The site is a single Astro app deployed as a Cloudflare Worker. The landing (`/`, `/es`), the community page, events, projects and the docs (`/docs/*`) are SSR-rendered at the edge. The hero signup form is a vanilla script (`src/scripts/signup.ts`) that posts to the same-origin route `/api/signup`, which validates with the shared rules in `src/lib/signup.ts`, rate limits per IP, and proxies to cloud-api's public `POST /api/auth/signup`. Pricing and nav CTAs link to `https://cloud.nan.builders/signup?plan=<product>`. Static assets are served from the `ASSETS` binding configured in `wrangler.jsonc`.

See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full breakdown of rendering modes, route inventory, security model, and member lifecycle.

Expand All @@ -26,23 +25,14 @@ See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full breakdown of rendering m
```
website/
├── src/
│ ├── pages/
│ │ ├── index.astro # Landing (SSR)
│ │ ├── community.astro # Community tier page (SSR)
│ │ ├── api/
│ │ │ ├── waitlist.ts # POST — waitlist signup
│ │ │ └── community-signup.ts # POST — community tier signup
│ │ └── docs/ # SSR docs: index, getting-started, models,
│ │ # examples, api, agents
│ ├── components/
│ │ ├── landing/ # Hero, Pricing, WaitlistForm, FaqAccordion,
│ │ │ # LanguageSwitcher, LoginButton, etc.
│ │ ├── docs/ # CodeBlock, RateLimits
│ │ └── ui/ # Button
│ ├── layouts/ # Base, Docs, Page
│ ├── lib/ # waitlist, communitySignup, email, i18n
│ ├── styles/ # Tailwind theme + global CSS
│ ├── tests/ # Vitest suites (api/, lib/)
│ ├── pages/ # Landing, community, legal, events, projects, docs (EN root, ES under /es)
│ │ └── api/ # signup, auth/login-request, events proxy, docs manifest
│ ├── components/ # nan/ (shell + home sections), docs/, events/
│ ├── scripts/ # Vanilla client scripts (signup form, reveal, dither, ...)
│ ├── layouts/ # NanBase, NanPage, Docs, NanLegal
│ ├── lib/ # signup, signupServer, i18n, docs, events, rate limits
│ ├── styles/ # Tokens, system CSS, Tailwind theme
│ ├── tests/ # Vitest suites
│ └── env.d.ts # Cloudflare env typings
├── i18n/ # en.json, es.json translation catalogs
├── public/ # Static assets, favicons, _routes.json
Expand All @@ -60,7 +50,7 @@ website/

- Node.js `>=22.12.0` (uses native `--env-file=` in scripts)
- npm (the lockfile is `package-lock.json`; do not switch package managers)
- Access to a `cloud-api` instance (default: `https://cloud-api.nan.builders`). Without `CLOUD_API_WAITLIST_KEY` the signup form will fail at the API call, but the site will still render fine for visual work.
- Access to a `cloud-api` instance (default: `https://cloud-api.nan.builders`) for the signup route to work end to end. Without `CLOUD_API_URL` the form answers with a generic error, but the site renders fine for visual work.

### Setup

Expand Down Expand Up @@ -93,12 +83,10 @@ Runtime variables are declared in `src/env.d.ts` and consumed via `Astro.locals.

| Name | Required | Where it's used | Description |
|---|---|---|---|
| `RESEND_API_KEY` | runtime | `src/lib/email.ts` | Resend API key for transactional email (waitlist + community signup confirmation). |
| `RESEND_FROM_EMAIL` | runtime | `src/lib/email.ts` | `From` address used by Resend. |
| `CLOUD_API_URL` | runtime | `src/lib/waitlist.ts`, `src/lib/communitySignup.ts` | Base URL of the `cloud-api` backend (e.g. `https://cloud-api.nan.builders`). |
| `CLOUD_API_WAITLIST_KEY` | runtime | `src/lib/waitlist.ts`, `src/lib/communitySignup.ts` | API key for the `cloud-api` waitlist/community registration endpoints. |
| `CLOUD_API_URL` | runtime | `src/lib/signupServer.ts`, `src/lib/events.ts` | Base URL of the `cloud-api` backend (e.g. `https://cloud-api.nan.builders`). |
| `RATE_LIMIT_RPM` / `RATE_LIMIT_PARALLEL` | optional | `src/lib/rateLimits.ts` | Per-key limits shown in the docs; defaults apply when unset. |

All four are listed (without values) in `.env.example`. You must fill them locally if you want the API routes to work end-to-end. None of these secrets should ever be committed.
`RESEND_API_KEY`, `RESEND_FROM_EMAIL` and `CLOUD_API_WAITLIST_KEY` are no longer read by the site and can be removed from the Worker. None of these values should ever be committed.

## Testing

Expand All @@ -107,14 +95,7 @@ npm test # run all Vitest suites
npm run test:watch
```

Vitest config: [`vitest.config.ts`](./vitest.config.ts). Tests are picked up from `src/**/*.test.ts`. Current suites:

- `src/tests/api/waitlist.test.ts`
- `src/tests/api/community-signup.test.ts`
- `src/tests/lib/waitlist.test.ts`
- `src/tests/lib/email.test.ts`
- `src/lib/communitySignup.test.ts`
- `src/components/landing/waitlistForm.helpers.test.ts`
Vitest config: [`vitest.config.ts`](./vitest.config.ts) (a `unit` project for `src/**/*.test.ts` and a `render` project for `src/**/*.render.test.ts`). The open-signup suites are `src/tests/lib/signup.test.ts`, `src/tests/api/signup.test.ts`, `src/tests/landing/signupForm.test.ts` and `src/tests/landing/openSignup.test.ts`.

## Build and local preview

Expand Down
119 changes: 60 additions & 59 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
{
"community": {
"label": "// community",
"pageTitle": "NaN Community",
"title": "Join the community while you wait for an inference seat.",
"subtitle": "Discord access, conversations with builders, and a reserved spot in line for GPU access when capacity frees up.",
"priceLabel": "14,99€ / month",
"priceTaxNote": "Taxes included · billed in euros from any region",
"includesLabel": "What's included",
"notIncludedLabel": "Not included",
"notIncluded": {
"0": "Inference service access (GPUs)",
"1": "An included workspace (workspace slots are sold separately)",
"2": "API key for model calls"
},
"faqUpgradeQ": "How do I move to the inference plan later?",
"faqUpgradeA": "When GPU capacity frees up, we'll invite you manually. In the meantime you can request the upgrade from your member dashboard.",
"faqCancelQ": "Can I cancel?",
"faqCancelA": "Yes, anytime from your member dashboard. No commitment.",
"backToLanding": "← Back to nan.builders",
"emailLabel": "email",
"emailPlaceholder": "you@email.com",
"regionLabel": "region",
"regionDefault": "Select your region",
"regionEU": "Europe",
"regionUSA": "USA",
"regionLATAM": "LATAM",
"submit": "Join the community",
"submitting": "Processing…",
"redirecting": "Redirecting to checkout…",
"alreadySubscribed": "You're already in. If you didn't get a receipt, write to us at",
"honeypot": "Do not fill this field",
"errorInvalidEmail": "Invalid email. Check the format.",
"errorInvalidRegion": "Select a region.",
"errorRateLimited": "Too many attempts. Wait a minute.",
"errorServer": "Something went wrong. Try again in a moment.",
"errorNetwork": "No connection. Try again.",
"footer": "Cancel anytime.",
"alreadyMemberLabel": "// already a member"
"footer": "Cancel anytime."
},
"events": {
"backToEvents": "← Events",
Expand Down Expand Up @@ -278,7 +244,7 @@
"nan": {
"meta": {
"title": "NaN — Build without limits",
"description": "A community of builders sharing GPUs to run open models at a flat rate. OpenAI-compatible API, zero logs, spots limited by compute.",
"description": "A community of builders sharing GPUs to run open models at a flat rate. OpenAI-compatible API, zero logs, open signup.",
"siteName": "NaN",
"ogLocale": "en_US"
},
Expand Down Expand Up @@ -311,26 +277,60 @@
"events": "Events",
"projects": "Projects",
"docs": "Docs",
"login": "Login"
"login": "Login",
"signup": "Create account"
},
"hero": {
"line1": "Build without",
"line2": "limits.",
"sub": "Don't run out of tokens. Ever.",
"body": "NaN is the community of builders sharing GPUs to run AI open models. Build whatever you need, without paying per token. You can just do things.",
"spots": "Limited spots",
"waitline": "Join the waitlist to unlock your subscription. Just leave your email.",
"emailPlaceholder": "you@email.dev",
"regionLabel": "Select your Region",
"submit": "Join waitlist",
"spots": "Open signup",
"signupLine": "Create your free account. Every product unlocks with a plan, when you're ready.",
"labelFullName": "Full name",
"labelEmail": "Email",
"labelJobRole": "Role",
"jobRolePlaceholder": "Select your role",
"labelJobRoleOther": "Your role",
"labelXUrl": "X profile",
"labelLinkedinUrl": "LinkedIn profile",
"optional": "optional",
"requiredNote": "required",
"phFullName": "Ada Lovelace",
"phEmail": "you@email.dev",
"phJobRoleOther": "What you do",
"phXUrl": "https://x.com/you",
"phLinkedinUrl": "https://linkedin.com/in/you",
"jobRoles": {
"software_engineer": "Software engineer",
"ai_engineer": "AI engineer",
"data_scientist": "Data scientist",
"devops_sre": "DevOps / SRE",
"founder": "Founder",
"product": "Product",
"designer": "Designer",
"researcher": "Researcher",
"student": "Student",
"other": "Other"
},
"submit": "Create account",
"sending": "Sending…",
"okRegistered": "You're on the waitlist.",
"okText": "We'll approve your spot in the next few days. Keep an eye on your inbox ^__^",
"okTitle": "Check your inbox to confirm and sign in.",
"okText": "If the email doesn't arrive in a couple of minutes, check your spam folder.",
"errFullName": "Tell us your name (up to 120 characters).",
"errEmail": "That email does not look valid.",
"errRegion": "Pick a region.",
"errJobRole": "Pick your role.",
"errJobRoleOther": "Tell us your role (up to 80 characters).",
"errXUrl": "Use your X profile link, like https://x.com/you.",
"errLinkedinUrl": "Use your LinkedIn profile link, like https://linkedin.com/in/you.",
"errFields": "Check the highlighted fields.",
"errNetwork": "The network failed, not you. Try again.",
"errGeneric": "Something broke on our side. Try again in a bit.",
"errRateLimited": "Too many attempts. Wait a minute."
"errRateLimited": "Too many attempts. Wait a few minutes and try again.",
"legalPre": "By creating an account you accept the",
"legalTerms": "Terms",
"legalAnd": "and the",
"legalPrivacy": "Privacy Policy"
},
"why": {
"label": "what is nan",
Expand All @@ -340,7 +340,7 @@
"p": [
"Closed-model APIs are convenient but expensive and limited. Open models are free but running them seriously requires hardware almost no one has at home.",
"NaN solves this by sharing powerful GPUs among builders who actually use them, splitting the real cost of infrastructure.",
"A closed community by design, limited to the real capacity of shared GPUs. When it fills up, we open a waitlist.",
"Open to anyone who wants to build. Creating an account is free; the cluster, the community and the workspaces switch on with a plan.",
"The community lives on Discord. The website only manages access. Every member pays to be here. Only those who truly want to build make it in."
]
},
Expand All @@ -360,7 +360,7 @@
"sub": "The full open-model inference stack, voted by the community every quarter. Data stays in the EU, with nothing logged.",
"badgePremium": "premium tier",
"unmetered": "unmetered",
"cta": "Join the waitlist"
"cta": "Create your account"
},
"privacy": {
"label": "privacy",
Expand Down Expand Up @@ -394,14 +394,14 @@
"Events, workshops and hackathons",
"Voice in the quarterly model vote"
],
"memberCond": "Month to month, no commitment. Billed in euros from any region. Limited spots: when it fills up, waitlist.",
"memberCta": "Join the waitlist",
"memberCond": "Month to month, no commitment. Billed in euros from any region.",
"memberCta": "Get inference",
"communityIncludes": [
"Discord access",
"Member-only channels",
"Live discussions, builds, AMAs",
"Events, workshops and hackathons",
"A reserved spot in line for inference"
"Upgrade to inference whenever you want"
],
"communityCond": "No commitment. Cancel anytime. Billed in euros from any region.",
"communityCta": "Join the community",
Expand All @@ -414,13 +414,13 @@
"Everything in nan_member",
"Access switches on a few minutes after payment"
],
"premiumCond": "GLM 5.3 premium (200€/mo) is offered to members as seats open. Join the waitlist and we will flag you for this tier.",
"premiumCta": "Join the waitlist",
"premiumCond": "Month to month, no commitment. Billed in euros from any region.",
"premiumCta": "Go premium",
"premiumBadge": "glm 5.3 · premium",
"workspaces": {
"label": "workspaces · add-on",
"title": "Your own cloud machine.",
"sub": "A dedicated, isolated dev box reachable over SSH and from the browser, with Pi, Hermes, gentle-shell and Herdr ready to use. Any paying member can buy slots; each slot keeps one workspace running.",
"sub": "A dedicated, isolated dev box reachable over SSH and from the browser, with Pi, Hermes, gentle-shell and Herdr ready to use. Anyone with a NaN account can buy slots; each slot keeps one workspace running.",
"per": "/mo",
"headSize": "size",
"headSpecs": "vCPU · RAM · disk",
Expand All @@ -432,7 +432,8 @@
"Backups are optional: one snapshot a day, kept 7 days, restore it yourself.",
"Monthly, billed in euros. Cancel a slot anytime."
],
"cta": "Read the workspaces guide"
"cta": "Read the workspaces guide",
"signupCta": "Get a workspace"
}
},
"who": {
Expand Down Expand Up @@ -491,22 +492,22 @@
},
{
"q": "What is a workspace?",
"a": "Your own cloud machine for development: a dedicated, isolated box you reach over SSH or from the browser, with coding agents such as Pi and Hermes ready to use. Any paying member can buy workspace slots from 4,99€ a month, and the premium plan includes one free Micro workspace. A slot does not include inference: agents use NaN models only on an inference plan. The workspaces guide in the docs has sizes, prices, network rules and how backups work."
"a": "Your own cloud machine for development: a dedicated, isolated box you reach over SSH or from the browser, with coding agents such as Pi and Hermes ready to use. Anyone with a NaN account can buy workspace slots from 4,99€ a month, and the premium plan includes one free Micro workspace. A slot does not include inference: agents use NaN models only on an inference plan. The workspaces guide in the docs has sizes, prices, network rules and how backups work."
},
{
"q": "What payment methods do you accept?",
"a": "Credit or debit card. New memberships are billed in euros from any region, taxes included; subscriptions opened earlier in US dollars keep their original price. No commitment: cancel anytime."
},
{
"q": "How do I get in?",
"a": "Leave your email on the waitlist and we'll write when a spot opens. There's a waitlist because there are no more GPUs: when compute comes in, people come in."
"a": "Create your account with your email: it's free and takes a minute. Then pick what you need (community, inference, premium or a workspace) and it switches on right after payment."
}
]
},
"footer": {
"cta1": "Start building.",
"cta2": "Start burning tokens.",
"go": "Join the waitlist",
"go": "Create your account",
"privacy": "Privacy",
"terms": "Terms",
"cookies": "Cookies",
Expand All @@ -533,7 +534,7 @@
"title2": "out of tokens.",
"body": "The address you typed doesn't exist, or it never did. Either way, there's nothing to render here. You, though, still fit here.",
"back": "Back to home",
"join": "Join the waitlist"
"join": "Create your account"
},
"projects": {
"metaTitle": "Projects — NaN",
Expand Down
Loading
Loading