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
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# cloud-api backend (Go service)
# Default is the production endpoint; override if running cloud-api locally.
CLOUD_API_URL=https://cloud-api.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.
# Used by the events pages (SSR + /api/events proxy) and the events login.

# 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
50 changes: 15 additions & 35 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@

NaN is a shared GPU infrastructure project for a paid community. The website covers:

1. **Landing** — Presentation, models, pricing, and account signup (open signup)
1. **Landing** — Presentation, models, pricing, and links to account creation (open signup)
2. **Documentation** — Public API docs for connecting tools (Cursor, Cline, OpenCode, etc.)

All community activity (chat, forums, events, model voting) lives in Discord.
Account creation is open: anyone can create a free account from the home form, and every product (community, inference, premium, workspaces, spaces) unlocks with a purchase on the [NaN Platform](https://cloud.nan.builders/), which also handles payment, onboarding and API keys. Spec: `platform/cloud-api/docs/OPEN-SIGNUP.md`.
Account creation is open: anyone can create a free account on cloud.nan.builders (the site only links there: hero, nav, pricing, footer), and every product (community, inference, premium, workspaces, spaces) unlocks with a purchase on the [NaN Platform](https://cloud.nan.builders/), which also handles payment, onboarding and API keys. Spec: `platform/cloud-api/docs/OPEN-SIGNUP.md`.

## Tech Stack

| Layer | Technology | Why |
|-------|-----------|-----|
| Framework | Astro v6.0.8 (SSR mode, `output: 'server'`) | Static by default, SSR where needed |
| Interactive bits | Vanilla TS scripts (home, nav, signup form); Preact islands only where needed (events) | Zero framework JS on the marketing pages |
| Interactive bits | Vanilla TS scripts (home effects, nav); Preact islands only where needed (events) | Zero framework JS on the marketing pages |
| Styling | Tailwind CSS v4.2.2 (Vite plugin) | Dark theme, technical typography |
| Code highlighting | rehype-pretty-code + shiki | Syntax highlighting in docs |
| Deployment | Cloudflare Workers (static assets + SSR) | Assets from CDN, SSR/API from Workers edge |
Expand All @@ -40,7 +40,6 @@ nan.builders (Cloudflare Worker)
│ └── Static assets (CSS, JS, fonts, images, favicon)
│
└── Worker (Edge Functions)
├── /api/signup POST — account signup (validates, proxies to cloud-api /api/auth/signup)
├── /api/auth/login-request POST — magic-link login from the events pages (proxy)
├── /api/events/* events API proxy
├── /api/docs/* docs manifest + markdown for the Discord bot
Expand All @@ -56,10 +55,9 @@ nan.builders (Cloudflare Worker)

| Route | Mode | Client JS |
|-------|------|-----------|
| `/`, `/es` | SSR (not prerendered) | Vanilla scripts: hero dither, signup form (`scripts/signup.ts`), reveal |
| `/`, `/es` | SSR (not prerendered) | Vanilla scripts: hero dither, reveal (the hero CTA's `?plan=` is resolved in SSR, no JS) |
| `/community`, `/es/community` | SSR (not prerendered) | None beyond the shared shell (CTA links to the platform signup) |
| `/docs*` | SSR (not prerendered) | Minimal (sidebar mobile menu, TOC, copy-to-clipboard) |
| `/api/signup` | Server endpoint | None |

## Pricing (Current)

Expand Down Expand Up @@ -105,7 +103,7 @@ Static sections, SSR-rendered (`src/pages/_home.astro`, components in `src/compo
| Section | Component | Client JS |
|---------|-----------|-----------|
| Nav (Login + Create account) | `nan/Nav.astro` | Small inline script (mobile menu) |
| Hero + signup form (`#signup`) | `Hero.astro` | `scripts/signup.ts` (vanilla) + canvas dither |
| Hero: Create account + Log in buttons | `Hero.astro` | Canvas dither only |
| What is NaN | `WhyNan.astro` | None |
| Built with NaN | `BuiltWithNan.astro` | None |
| Models | `Models.astro` | None |
Expand All @@ -130,25 +128,9 @@ Sidebar navigation comes from each guide's frontmatter (`src/content.config.ts`)

### API Endpoints

#### `POST /api/signup`

Creates a NaN account through cloud-api's public `POST /api/auth/signup` (`OPEN-SIGNUP.md` §2.3). The browser cannot call cloud-api directly because the CSP is `connect-src 'self'`.

- **Input (JSON):** `{ fullName, email, jobRole, jobRoleOther, xUrl, linkedinUrl, plan?, _hp }`
- **Validation** (`validateSignup()` in `src/lib/signup.ts`, shared with the form):
- `fullName` 1-120 chars, `email` valid and not on the blocked-domain list, `jobRole` in `software_engineer | ai_engineer | data_scientist | devops_sre | founder | product | designer | researcher | student | other`; `jobRoleOther` 1-80 chars, required only when `jobRole` is `other`.
- `xUrl` optional, `https://x.com/...` or `https://twitter.com/...`; `linkedinUrl` optional, `https://(www.)linkedin.com/in/...`. Missing scheme, `http://`, `www.` and LinkedIn country subdomains are normalized before sending.
- `plan` optional (`community | inference | premium | workspace | space`); an unknown plan is dropped, never an error. The form reads it from `?plan=` (and the legacy `?premium=1`).
- **Honeypot:** `_hp` filled → same `202` as a real signup, nothing forwarded.
- **Rate limit:** 5 valid submissions per IP per 10 minutes, in memory per isolate (best effort; cloud-api limits per IP and per email behind it). Invalid submissions do not count.
- **Upstream call:** `POST ${CLOUD_API_URL}/api/auth/signup` with `Origin: https://nan.builders`, the client IP in `x-forwarded-for` / `cf-connecting-ip`, and the visitor country in `cf-ipcountry` (cloud-api derives the billing region from it). No API key: the endpoint is public.
- **Content type:** the media type must be exactly `application/json` (parameters such as `charset` allowed). The hero form has `method="post" action="/api/signup"`, so without JS the browser posts form data instead of putting it in a URL; that post gets a `415` static HTML page (dark, no JS, own CSP; Spanish when posted from `/es` or, without a same-origin Referer, when Accept-Language prefers it) linking to `https://cloud.nan.builders/signup`, and the body is never read (`src/lib/signupNoJs.ts`).
- **Responses:**
- `202`: `{ ok: true }` for every accepted signup, new or known email (no account enumeration). The form shows "Check your inbox to confirm and sign in"; cloud-api sends the English magic link.
- `400`: `{ ok: false, error: 'invalid_input', fields: { <field>: 'required' | 'invalid' | 'too_long' } }`
- `413` / `415`: oversized body / not `application/json` (`{ ok: false, error: 'unsupported_media_type' }`, or the HTML page for a no-JS form post)
- `429`: `{ ok: false, error: 'rate_limited' }`
- `502`: `{ ok: false, error: 'server_error' }` (upstream error or unreachable; details only in the Worker log)
#### Account creation

There is no signup route on the site. The hero's **Create account** button links to `https://cloud.nan.builders/signup`, carrying the landing's `?plan=` when it is one of `community | inference | premium | workspace | space` (the legacy `?premium=1` maps to `plan=premium`; anything else is dropped). **Log in** links to `https://cloud.nan.builders/`. Models, footer and 404 CTAs link to the bare signup; pricing cards add their plan. Links are built with `signupUrl()` / `heroSignupHref()` in `src/lib/signup.ts`. The platform collects the profile, sends the magic link and runs checkout.

## Internationalization

Expand All @@ -159,7 +141,7 @@ The site is bilingual: English at the root (`/`) and Spanish under `/es`. Catalo
| Mechanism | Implementation |
|-----------|---------------|
| CSP | Strict policy in `NanBase.astro` (`connect-src 'self'`, no external scripts, no frames, no objects) |
| Bot protection | Honeypot field (`_hp`) on the signup form + per-IP rate limiting at the edge + cloud-api per-IP/per-email limits + Origin allowlist |
| Bot protection | No public write form on the marketing pages; signup lives on cloud.nan.builders (cloud-api per-IP/per-email limits). Events forms go through `/api/events/*` |
| IP detection | `cf-connecting-ip` only (never `x-forwarded-for` on Cloudflare) |
| Secrets | All in Cloudflare Pages dashboard via `wrangler secret put`, never in code |
| Error handling | Intentionally opaque error responses (no internal details leaked) |
Expand All @@ -174,7 +156,6 @@ website/
│ │ ├── _community.astro, community.astro # Community page (CTA to platform signup)
│ │ ├── privacy.astro, terms.astro, cookies.astro # Legal (+ es/ versions)
│ │ ├── api/
│ │ │ ├── signup.ts # POST — account signup proxy
│ │ │ ├── auth/login-request.ts # POST — magic-link login proxy (events)
│ │ │ ├── events/[...path].ts # Events API proxy
│ │ │ └── docs/ # Docs manifest + markdown
Expand All @@ -183,10 +164,9 @@ website/
│ │ ├── nan/ # Nav, Footer, CookieBanner, home/ sections
│ │ ├── docs/ # Docs shell components
│ │ └── events/ # Events islands (Preact)
│ ├── scripts/ # Vanilla client scripts (signup, reveal, dither, ...)
│ ├── scripts/ # Vanilla client scripts (reveal, dither, ...)
│ ├── lib/
│ │ ├── signup.ts # Signup validation/normalization/parsing (client + server)
│ │ ├── signupServer.ts # Edge rate limit + cloud-api call (server only)
│ │ ├── signup.ts # Links to the platform signup (plans, ?plan= carry-through)
│ │ ├── i18n.ts # Translation helpers
│ │ └── ... # docs, events, rate limits, models
│ ├── layouts/ # NanBase (CSP, head), NanPage (shell), Docs, NanLegal
Expand All @@ -204,7 +184,7 @@ website/

| Variable | Type | Purpose |
|----------|------|---------|
| `CLOUD_API_URL` | `string` | Backend API base URL (e.g. `https://cloud-api.nan.builders`), used by `/api/signup`, the events proxy and login |
| `CLOUD_API_URL` | `string` | Backend API base URL (e.g. `https://cloud-api.nan.builders`), used by the events proxy, events SSR and the events login |
| `RATE_LIMIT_RPM` / `RATE_LIMIT_PARALLEL` | `string` (optional) | Per-key limits shown in the docs (defaults in `src/lib/rateLimits.ts`) |

`RESEND_API_KEY`, `RESEND_FROM_EMAIL` and `CLOUD_API_WAITLIST_KEY` are no longer used by the site (the waitlist and its confirmation email are gone; cloud-api sends the sign-in email).
Expand Down Expand Up @@ -249,9 +229,9 @@ npm run test:watch # Watch mode
```

Tests are in `src/**/*.test.ts` (Node) and `src/**/*.render.test.ts` (Astro Container API). Open-signup suites:
- `src/tests/lib/signup.test.ts` — validation, normalization, plan, response parsing
- `src/tests/api/signup.test.ts` — `/api/signup` proxy, validation, honeypot, rate limit, error mapping
- `src/tests/landing/signupForm.test.ts` — hero form markup, script wiring, copy in both locales
- `src/tests/lib/signup.test.ts` — plans, `?plan=` parsing, signup URLs
- `src/tests/landing/heroCta.test.ts` — hero has no form, CTA hrefs and copy, dead code gone, no `#signup` links
- `src/tests/landing/heroCta.server.test.ts` — SSR hero hrefs with `?plan=` / `?premium=1`, `POST /api/signup` is a 404
- `src/tests/landing/openSignup.test.ts` — CTA targets, no waitlist copy, legal pages
- `src/tests/layouts/noWebSearch.test.ts` — web search stays removed

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NaN Website

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.
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 and lives on cloud.nan.builders: the site links there (hero, nav, pricing, footer), and every product unlocks with a plan.

## Tech stack

Expand All @@ -16,7 +16,7 @@ Marketing site and public documentation for [nan.builders](https://nan.builders)

## Architecture

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`.
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. There is no signup form or route on the site: the hero's Create account button links to `https://cloud.nan.builders/signup` (carrying a valid `?plan=` from the landing URL; the legacy `?premium=1` maps to `plan=premium`), Log in links to `https://cloud.nan.builders/`, and pricing cards link to `signup?plan=<product>`. Links are built in `src/lib/signup.ts`. 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,11 +26,11 @@ See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full breakdown of rendering m
website/
├── src/
│ ├── pages/ # Landing, community, legal, events, projects, docs (EN root, ES under /es)
│ │ └── api/ # signup, auth/login-request, events proxy, docs manifest
│ │ └── api/ # auth/login-request, events proxy, docs manifest
│ ├── components/ # nan/ (shell + home sections), docs/, events/
│ ├── scripts/ # Vanilla client scripts (signup form, reveal, dither, ...)
│ ├── scripts/ # Vanilla client scripts (reveal, dither, ...)
│ ├── layouts/ # NanBase, NanPage, Docs, NanLegal
│ ├── lib/ # signup, signupServer, i18n, docs, events, rate limits
│ ├── lib/ # signup links, i18n, docs, events, rate limits
│ ├── styles/ # Tokens, system CSS, Tailwind theme
│ ├── tests/ # Vitest suites
│ └── env.d.ts # Cloudflare env typings
Expand All @@ -50,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`) 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.
- Access to a `cloud-api` instance (default: `https://cloud-api.nan.builders`) for the events pages. Without `CLOUD_API_URL` the events pages fail to load, but the landing and docs render fine.

### Setup

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

| Name | Required | Where it's used | Description |
|---|---|---|---|
| `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`). |
| `CLOUD_API_URL` | runtime | `src/lib/events.ts`, `src/pages/api/events/[...path].ts`, `src/pages/api/auth/login-request.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. |

`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.
Expand All @@ -95,7 +95,7 @@ npm test # run all Vitest suites
npm run test:watch
```

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`.
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 signup-link suites are `src/tests/lib/signup.test.ts`, `src/tests/landing/heroCta.test.ts`, `src/tests/landing/heroCta.server.test.ts` and `src/tests/landing/openSignup.test.ts`.

## Build and local preview

Expand Down
48 changes: 2 additions & 46 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,52 +289,8 @@
"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": "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…",
"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.",
"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 few minutes and try again.",
"legalPre": "By creating an account you accept the",
"legalTerms": "Terms",
"legalAnd": "and the",
"legalPrivacy": "Privacy Policy"
"ctaPrimary": "Create account",
"ctaSecondary": "Log in"
},
"why": {
"label": "what is nan",
Expand Down
Loading
Loading