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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ ONECLICK_BASE_URL=https://1click.chaindefuser.com
ONECLICK_JWT=
# Empty → embedded PGlite
DATABASE_URL=
# Hosted previews only: their own database. Preview/Development deployments never use DATABASE_URL.
PREVIEW_DATABASE_URL=
SESSION_SECRET=
PUBLIC_APP_URL=http://localhost:3000
# Optional: wallet network for the browser (defaults to NEAR's main network)
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ PUBLIC_APP_URL=
CRON_SECRET= # ≥ 16 chars; Vercel sends it to the cron route
```

**Hosting (K9.1, decided 2026-11-05):** web on Vercel (project `stack-savings`, Node 22), Postgres on Neon (via the Vercel integration, pooled `DATABASE_URL`). `pnpm vercel-build` runs `drizzle-kit migrate` before `next build`; migrations SQL is traced into the API functions; `getDb()` refuses to start hosted without `DATABASE_URL`. `.vercelignore` keeps contracts and local data out of the upload. The worker needs a long-running host; it is only needed for real mode.
**Hosting (K9.1, decided 2026-11-05):** web on Vercel (project `stack-savings`, Node 22), Postgres on Neon (via the Vercel integration, pooled `DATABASE_URL`). `pnpm vercel-build` (`scripts/vercel-build.mjs`) runs `drizzle-kit migrate` before `next build` on production builds only. The Neon integration also gives Preview and Development the production `DATABASE_URL`; `databaseUrlFor()` ignores it there and uses only `PREVIEW_DATABASE_URL` (K13.5). `pnpm demo` clears `DATABASE_URL`, so a pulled `.env.local` can't point the demo at production; migrations SQL is traced into the API functions; `getDb()` refuses to start hosted without `DATABASE_URL`. `.vercelignore` keeps contracts and local data out of the upload. The worker needs a long-running host; it is only needed for real mode.

---

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ Add an admin withdraw path · use an unverified quote · show unconfirmed status
`NEAR_RPC_URL FACTORY_ACCOUNT_ID USDC_CONTRACT_ID KEEPER_ACCOUNT_ID KEEPER_PRIVATE_KEY ONECLICK_BASE_URL ONECLICK_JWT DATABASE_URL SESSION_SECRET PUBLIC_APP_URL CRON_SECRET`

## Hosting
Vercel (web, `pnpm vercel-build` migrates first) + Neon Postgres + daily cron `/api/cron/cleanup-demo`. Hosted demo: https://stack-savings.vercel.app/demo. Worker needs a long-running host (real mode only).
Vercel (web; `pnpm vercel-build` migrates only on production builds, and Preview/Development never use production's `DATABASE_URL`, only `PREVIEW_DATABASE_URL`) + Neon Postgres + daily cron `/api/cron/cleanup-demo`. Hosted demo: https://stack-savings.vercel.app/demo. Worker needs a long-running host (real mode only).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Other protections: real quotes are refused unless their signature verifies (fail

Being honest about what is and isn't live:

- **Pre-launch review (2026-11-08):** a security and reliability review found nine issues; they are fixed one at a time and tracked as ROADMAP Phase 13. Fixed so far: a buy whose deposit never reaches NEAR Intents now expires after 30 minutes instead of blocking the plan forever; when NEAR gives no clear result for a buy (a timeout after sending), it stays in flight until NEAR Intents confirms it, instead of being shown as failed with nothing spent; signing out now ends the session on every device, so a copied session cookie stops working.
- **Pre-launch review (2026-11-08):** a security and reliability review found nine issues; they are fixed one at a time and tracked as ROADMAP Phase 13. Fixed so far: a buy whose deposit never reaches NEAR Intents now expires after 30 minutes instead of blocking the plan forever; when NEAR gives no clear result for a buy (a timeout after sending), it stays in flight until NEAR Intents confirms it, instead of being shown as failed with nothing spent; signing out now ends the session on every device, so a copied session cookie stops working; preview deployments and `pnpm demo` can no longer read or migrate the production database.

- **Contracts are built and tested but not deployed yet** (ROADMAP K1.13). Real mode needs deployed contracts, funded keeper/factory accounts and a 1Click partner JWT.
- **Quote signatures:** real 1Click quotes work without a partner key, but `verifyQuoteSignature()` in the newest SDK (0.1.26) returns false on dry and real quotes (tested 2026-11-05). Real mode refuses unverified quotes, so real funding and buys stay closed until 1Click confirms the current signing key or hash (ROADMAP K3.5). The demo is not affected.
Expand Down
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
| 10. README & public repo | Nov 12–13 | 100% | ✅ done (hosted URL added) |
| 11. Launch: video, post, submission | Nov 13–14 | 57% | in progress (video, post and submission need the human) |
| 12. After submission | Nov 14–25 | 0% | not started |
| 13. Launch review fixes | Nov 8–13 | 33% | in progress (pre-launch security and reliability review) |
| **Overall (MVP)** | | **~77%** | |
| 13. Launch review fixes | Nov 8–13 | 44% | in progress (pre-launch security and reliability review) |
| **Overall (MVP)** | | **~79%** | |

Phase % = done tasks ÷ non-dropped tasks in that phase. Overall = average of all 15 rows (D + phases 0–13; Phase 13 was added on 2026-11-08, which lowered the overall %). (Earlier entries over-estimated the overall %; recalculated 2026-11-04.)

Expand Down Expand Up @@ -213,8 +213,8 @@ From the pre-launch security and reliability review (2026-11-08), ranked with au
- [x] **K13.1** [#127](https://github.com/MFrancis-dev/stack/issues/127) Expire buys whose deposit never reaches 1Click · 2026-11-08: `pollBuys` marks a buy `expired` when 1Click still shows `PENDING_DEPOSIT` 30 min after it (quote deadline 10 min + margin); buys 1Click has seen are never expired; 3 tests
- [x] **K13.2** [#128](https://github.com/MFrancis-dev/stack/issues/128) Unclear `execute_buy` errors must not be recorded as failed (a timeout after broadcast showed "Nothing spent" while USDC left) · 2026-11-08: the NEAR adapter raises `BuyOutcomeUnknownError` unless the error is a clear failure (contract refusal, failed action, rejected transaction); the scheduler keeps that buy `sent` with a note and alerts; K13.1's expiry settles it if nothing arrived; 3 tests
- [ ] **K13.3** [#129](https://github.com/MFrancis-dev/stack/issues/129) ⛔ Vault creation spends the factory's NEAR for anyone who signs in (~1.5 NEAR per vault): owner pays storage, or create after the first deposit
- [x] **K13.4** [#130](https://github.com/MFrancis-dev/stack/issues/130) Make sessions revocable (sign-out only cleared the cookie) · 2026-11-08: sessions carry an issued-at time; sign-out sets `users.sessions_valid_after` (migration 0003) and every older session is rejected, real and demo; test signs in, signs out, checks the copied cookie fails and a new sign-in works
- [ ] **K13.5** [#131](https://github.com/MFrancis-dev/stack/issues/131) Keep preview and development off the production database
- [x] **K13.4** [#130](https://github.com/MFrancis-dev/stack/issues/130) Make sessions revocable (sign-out only cleared the cookie) · 2026-11-08 (with a CI fix: retry the `cargo-near` download, then build it from crates.io, after GitHub releases kept answering 504): sessions carry an issued-at time; sign-out sets `users.sessions_valid_after` (migration 0003) and every older session is rejected, real and demo; test signs in, signs out, checks the copied cookie fails and a new sign-in works
- [x] **K13.5** [#131](https://github.com/MFrancis-dev/stack/issues/131) Keep preview and development off the production database · 2026-11-08: `scripts/vercel-build.mjs` migrates only on production builds; `databaseUrlFor()` ignores `DATABASE_URL` on Preview/Development (uses `PREVIEW_DATABASE_URL` or refuses to start); `pnpm demo` clears `DATABASE_URL` (checked: Next keeps the empty value over `.env.local`); 3 tests
- [ ] **K13.6** [#132](https://github.com/MFrancis-dev/stack/issues/132) Cap demo starts and fast-forwards in the database
- [ ] **K13.7** [#133](https://github.com/MFrancis-dev/stack/issues/133) Rate limits shared across serverless instances
- [ ] **K13.8** [#134](https://github.com/MFrancis-dev/stack/issues/134) Bad ids and malformed cookies return 500
Expand All @@ -234,6 +234,7 @@ From the pre-launch security and reliability review (2026-11-08), ranked with au
## Update log
One line per contribution, newest first: `YYYY-MM-DD · who/agent · what changed · tasks`

- 2026-11-08 · agent · K13.5 previews and `pnpm demo` can't touch the production database (migrate on production builds only, `databaseUrlFor`, `PREVIEW_DATABASE_URL`); README, ARCHITECTURE, ESSENTIALS, `.env.example` updated · K13.5
- 2026-11-08 · agent · K13.4 revocable sessions: issued-at in the token, sign-out revokes every older session (`sessions_valid_after`, migration 0003); README status, ARCHITECTURE §4.1/§5, ESSENTIALS updated · K13.4
- 2026-11-08 · agent · K13.2 unclear `execute_buy` results stay in flight (`BuyOutcomeUnknownError`, `isClearTxFailure`) instead of being marked failed; README status and ARCHITECTURE §4.4 updated · K13.2
- 2026-11-08 · agent · Pre-launch review: new Phase 13 (9 tasks, issues #127–#135); K13.1 buys whose deposit never reaches 1Click expire after 30 min; README status updated · K13.1
Expand Down
20 changes: 18 additions & 2 deletions lib/db/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,28 @@ export function createDb({ databaseUrl, pgliteDataDir = DEFAULT_PGLITE_DIR }: Db

let shared: Db | undefined;

/**
* Which database this process may use (K13.5). On Vercel only Production uses `DATABASE_URL`: the
* Neon integration hands the production URL to Preview and Development too, and a preview must
* never read or write production data, so those use `PREVIEW_DATABASE_URL` or nothing.
*/
export function databaseUrlFor(env: Record<string, string | undefined>): string | undefined {
if (env.VERCEL && env.VERCEL_ENV !== "production") return env.PREVIEW_DATABASE_URL || undefined;
return env.DATABASE_URL || undefined;
}

/** Process-wide client for the app and worker. Tests should call `createDb` directly. */
export function getDb(): Db {
if (!shared) {
const databaseUrl = process.env.DATABASE_URL || undefined;
const databaseUrl = databaseUrlFor(process.env);
// Hosted platforms have a read-only or throwaway disk: the embedded database would lose data or crash.
if (!databaseUrl && process.env.VERCEL) throw new Error("DATABASE_URL is required when hosted (embedded PGlite is for local and CI only)");
if (!databaseUrl && process.env.VERCEL) {
throw new Error(
process.env.VERCEL_ENV === "production"
? "DATABASE_URL is required when hosted (embedded PGlite is for local and CI only)"
: "This preview has no database of its own: set PREVIEW_DATABASE_URL (it never uses production's)",
);
}
shared = createDb({ databaseUrl });
}
return shared;
Expand Down
23 changes: 23 additions & 0 deletions lib/db/databaseUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { databaseUrlFor } from "./client";

// Roadmap: K13.5
const PROD = "postgres://prod";
const PREVIEW = "postgres://preview";

describe("databaseUrlFor", () => {
it("uses DATABASE_URL in production on Vercel, and locally", () => {
expect(databaseUrlFor({ VERCEL: "1", VERCEL_ENV: "production", DATABASE_URL: PROD })).toBe(PROD);
expect(databaseUrlFor({ DATABASE_URL: PROD })).toBe(PROD);
});

it("never gives a preview or development deployment the production database", () => {
expect(databaseUrlFor({ VERCEL: "1", VERCEL_ENV: "preview", DATABASE_URL: PROD })).toBeUndefined();
expect(databaseUrlFor({ VERCEL: "1", VERCEL_ENV: "development", DATABASE_URL: PROD })).toBeUndefined();
expect(databaseUrlFor({ VERCEL: "1", VERCEL_ENV: "preview", DATABASE_URL: PROD, PREVIEW_DATABASE_URL: PREVIEW })).toBe(PREVIEW);
});

it("falls back to the embedded database when nothing is set locally", () => {
expect(databaseUrlFor({ DATABASE_URL: "" })).toBeUndefined();
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"vercel-build": "drizzle-kit migrate && next build",
"vercel-build": "node scripts/vercel-build.mjs",
"start": "next start",
"worker": "tsx --conditions=react-server worker/index.ts",
"demo": "tsx --conditions=react-server scripts/seed-demo.ts && next dev",
"demo": "DATABASE_URL= tsx --conditions=react-server scripts/seed-demo.ts && DATABASE_URL= next dev",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"format": "prettier --write .",
Expand Down
11 changes: 11 additions & 0 deletions scripts/vercel-build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Vercel build (K13.5): only production builds migrate the database. Preview and Development builds
// get the production DATABASE_URL from the Neon integration too, and must never change it.
import { execSync } from "node:child_process";

const env = process.env.VERCEL_ENV ?? "local";
if (env === "production") {
execSync("drizzle-kit migrate", { stdio: "inherit" });
} else {
console.log(`Skipping database migrations on a ${env} build: only production migrates its database.`);
}
execSync("next build", { stdio: "inherit" });
Loading