diff --git a/.env.example b/.env.example
index 682c7f3..a8b14c5 100644
--- a/.env.example
+++ b/.env.example
@@ -6,11 +6,9 @@ BUZZ_DEMO_MODE=false
DATABASE_URL=postgresql://buzz_sheet:replace-me@postgres.example.internal:5432/buzz_sheet?sslmode=require
DATABASE_POOL_SIZE=10
-# Better Auth + Google OAuth
+# Better Auth email/password administrator login
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=replace-with-at-least-32-random-bytes
-GOOGLE_CLIENT_ID=replace-with-google-client-id
-GOOGLE_CLIENT_SECRET=replace-with-google-client-secret
ADMIN_EMAIL=admin@example.com
# Redis remote cache, event transport, and outbox worker
diff --git a/Plan.md b/Plan.md
index 5c4d2a4..a50d2d6 100644
--- a/Plan.md
+++ b/Plan.md
@@ -96,7 +96,7 @@ The admin uses a visual Notion-style block editor with forms, cards, drag-and-dr
- Cache validated public page snapshots by their page and data-source version vector. Use an external Redis-backed Next.js remote cache and tag handler so both application replicas share cache state and invalidations. Keep admin routes dynamic.
- Write cache invalidation and SSE work to a transactional outbox with the content mutation. A retrying worker invalidates affected page, directory, and data-source tags only after the database commit, then publishes typed Redis events.
- Provide Redis-backed, invalidation-only SSE streams for public pages, the character directory, and authenticated admin sessions. Events carry only opaque IDs and versions; clients refetch authoritative state on connection, reconnection, or notification. Send 90-second heartbeats, close streams after 30 minutes so clients reconnect, and disable Traefik response buffering. SSE is never a source of correctness and never carries page content.
-- Authenticate through Google using Better Auth. Permit admin access only when the verified Google email equals `ADMIN_EMAIL`; repeat authorization checks in every mutation, media, and administrative endpoint.
+- Authenticate with Better Auth email and password. Permit registration and admin access only when the credential account email exactly equals `ADMIN_EMAIL`; use a temporary `/register` bootstrap page and repeat authorization checks in every mutation, media, and administrative endpoint.
- Upload PNG, JPEG, WebP, and GIF assets up to 20 MB through short-lived presigned requests. Serve them through same-origin `/media/[id]` responses. Allow public access only while an asset is referenced by a currently visible snapshot; otherwise require admin authentication. Retain objects while referenced by either current content or retained revisions.
- Applying a template clones independent pages, blocks, and data sources. Later template edits affect only future applications.
- Do not provide custom HTML, JavaScript, TypeScript, React, CSS, code blocks that execute, external scripts, arbitrary npm packages, or network-capable extensions.
@@ -135,8 +135,8 @@ flowchart LR
- Implement internally in four gates:
1. Database model, immutable data-source versions, revisions, transactional outbox, formula engine, and workbook-derived fixtures.
2. Public renderer, templates, and responsive shadcn admin editor.
- 3. Google authentication, reference-aware S3 media handling, Redis-backed caching and SSE, and conflict recovery.
- 4. Production builds, browser tests, Docker, migrations, Kubernetes, and CI.
+ 3. Better Auth email/password authentication, reference-aware S3 media handling, Redis-backed caching and SSE, and conflict recovery.
+ 4. Production builds, HTTP and integration tests, Docker, migrations, Kubernetes, and CI.
- Release to production once all four gates pass.
- During the public/editor gate, initialize shadcn with the `base-nova` preset before adding components. Add only the official components required by the implemented surface, and review generated component source and Base UI composition after each addition.
- Implement and commit each completed feature separately. Do not accumulate the project into one large commit.
@@ -167,7 +167,7 @@ flowchart LR
- Verify those 379 formulas with deterministic decimal results, then separately test precedence, subtraction, multiplication, percentages, unary signs, blank and missing references, cycles, dependency-propagated failures, rounding, and division by zero.
- Test block validation, schema migration, unknown-block fallback, templates, custom slugs, redirects, public notes, visibility, ordering, and revision restore.
- Test autosave debounce, serialization, transient retries, concurrent conflicts, checkpoint coalescing, named revisions, 30-day expiry, global data-source dependency updates, historical version pinning, “use latest,” and media retention.
-- Test Google admin restrictions and authorization on every write/media endpoint.
+- Test email/password registration restrictions and authorization on every write/media endpoint.
- Test upload limits, file validation, failed upload recovery, and same-origin media delivery.
- Test cached public snapshots and immediate dependency-aware invalidation following accepted autosaves.
- Test SSE authorization, public-event privacy, heartbeat, reconnection and authoritative refetch, duplicate or missed notifications, and directory/page/admin topics.
diff --git a/README.md b/README.md
index 64f9ab8..8c03cc0 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Production target: `https://sheet.sudloh.com`
- Restricted decimal formula engine with named references, dependency ordering,
cycle detection, typed failures, and presentation-only rounding.
- Read-only comparison charts with visible values and accessible table fallbacks.
-- Google authentication restricted to one verified `ADMIN_EMAIL`.
+- Better Auth email/password login restricted to the configured `ADMIN_EMAIL`.
- Private S3-compatible media uploads with reference-aware same-origin delivery.
- PostgreSQL transactions, immutable data-source versions, revisions, and a
retryable outbox.
@@ -37,7 +37,8 @@ formula fixtures. Their guide text and media are not imported or published.
| `/` | Searchable public character directory |
| `/[character]` | Redirect to the first visible page |
| `/[character]/[page]` | Render a public guide page |
-| `/admin/login` | Google administrator sign-in |
+| `/admin/login` | Administrator email/password sign-in |
+| `/register` | Temporary administrator account bootstrap |
| `/admin` | Character and page overview |
| `/admin/[character]/[page]` | Visual page editor |
| `/admin/templates` | Apply reusable guide templates |
@@ -56,7 +57,6 @@ Requirements:
- PostgreSQL
- Redis
- Private S3-compatible object storage
-- Google OAuth credentials
Install dependencies and create your local environment file:
@@ -66,16 +66,11 @@ cp .env.example .env
```
`.env.example` contains placeholders only. Configure `.env` yourself; it is
-ignored by Git and must never be committed. For Google OAuth, register this
-authorized redirect URI:
-
-```text
-http://localhost:3000/api/auth/callback/google
-```
-
-Use the production origin in place of `http://localhost:3000` for the deployed
-OAuth client. `BETTER_AUTH_URL` must exactly match that origin, and access is
-granted only when Google reports a verified email equal to `ADMIN_EMAIL`.
+ignored by Git and must never be committed. `BETTER_AUTH_URL` must exactly
+match the application origin. Visit `/register` once to create the credential
+account using the exact `ADMIN_EMAIL`, then use `/admin/login` for later access.
+The registration endpoint rejects every other email; remove the temporary page
+after the administrator account has been created.
Prepare the database and start the application:
@@ -90,7 +85,8 @@ reference workbook content.
### Fixture-only demo
-The UI can be inspected without PostgreSQL, Redis, S3, or Google by running:
+The UI can be inspected without PostgreSQL, Redis, S3, or authentication
+credentials by running:
```bash
BUZZ_DEMO_MODE=true bun run dev
@@ -210,8 +206,6 @@ Before the first rollout, a cluster administrator must provision a
```text
DATABASE_URL
BETTER_AUTH_SECRET
-GOOGLE_CLIENT_ID
-GOOGLE_CLIENT_SECRET
ADMIN_EMAIL
REDIS_URL
S3_ENDPOINT
diff --git a/app/register/page.tsx b/app/register/page.tsx
new file mode 100644
index 0000000..07dfff4
--- /dev/null
+++ b/app/register/page.tsx
@@ -0,0 +1,20 @@
+import { redirect } from "next/navigation";
+import { connection } from "next/server";
+
+import { RegisterCard } from "@/components/admin/register-card";
+import { SiteHeader } from "@/components/public/site-header";
+import { getAdminSession } from "@/lib/auth/server";
+
+export default async function RegisterPage() {
+ await connection();
+ if (await getAdminSession()) redirect("/admin");
+
+ return (
+