From bec00d2c9b4696a3e6fb5677e8c3c4b13e54bf70 Mon Sep 17 00:00:00 2001 From: sirily Date: Tue, 10 Mar 2026 14:58:01 +0300 Subject: [PATCH] chore: remove MVP positioning and align plan defaults --- AGENTS.md | 4 +- CODEX_STATUS.md | 2 +- README.md | 12 ++--- apps/bot/README.md | 2 +- docs/ops/deployment.md | 6 +-- .../{mvp-system-plan.md => system-plan.md} | 15 +++--- infra/caddy/Caddyfile | 2 +- packages/config/src/index.ts | 2 +- packages/db/README.md | 2 +- packages/db/src/auth-store.ts | 2 +- packages/db/src/bootstrap.ts | 50 ++++++++++++++++++- 11 files changed, 73 insertions(+), 26 deletions(-) rename docs/plan/{mvp-system-plan.md => system-plan.md} (90%) diff --git a/AGENTS.md b/AGENTS.md index 4247117..85c3dd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ This file governs the whole repository unless a deeper `AGENTS.md` overrides it. ## Source of truth Read these files before making architectural changes: -- `docs/plan/mvp-system-plan.md` +- `docs/plan/system-plan.md` - `docs/architecture/system-overview.md` - `docs/architecture/repository-layout.md` - `docs/ops/deployment.md` @@ -37,7 +37,7 @@ This repository is a TypeScript monorepo for `nproxy`, a crypto-subscription ima - Keep provider-specific HTTP code out of domain services. - Model user request attempts separately from provider-key attempts. - Preserve the chosen deployment target: single VPS with Docker Compose. -- Preserve the chosen billing model for MVP: manual crypto invoice renewal. +- Preserve the chosen billing model: manual crypto invoice renewal. - Preserve the chosen quota display contract: user-facing approximate buckets only. ## Required follow-up when changing key areas diff --git a/CODEX_STATUS.md b/CODEX_STATUS.md index b6df0a8..3a78831 100644 --- a/CODEX_STATUS.md +++ b/CODEX_STATUS.md @@ -182,7 +182,7 @@ ## Полезные файлы - `AGENTS.md` -- `docs/plan/mvp-system-plan.md` +- `docs/plan/system-plan.md` - `docs/architecture/system-overview.md` - `docs/ops/deployment.md` - `docs/ops/provider-key-pool.md` diff --git a/README.md b/README.md index d1cbf6e..fcce9d9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # nproxy -Planning scaffold for a crypto-subscription image gateway. +Product codebase for a crypto-subscription image gateway. -The repository is intentionally light on runtime code. Its current purpose is to store: -- the agreed MVP plan; -- the repository layout for future implementation; +The repository contains: +- runtime applications and shared packages; +- the agreed system plan and architecture documents; - operational notes for deployment, Telegram pairing, and provider key rotation; -- directory-scoped instructions so future Codex runs can implement against the same decisions. +- directory-scoped instructions so Codex runs implement against the same decisions. ## Chosen baseline - Product: B2C website @@ -25,7 +25,7 @@ The repository is intentionally light on runtime code. Its current purpose is to - `scripts/` operational helpers ## Read first -- `docs/plan/mvp-system-plan.md` +- `docs/plan/system-plan.md` - `docs/architecture/system-overview.md` - `docs/ops/deployment.md` - `CONTRIBUTING.md` diff --git a/apps/bot/README.md b/apps/bot/README.md index eb27565..73d3bc1 100644 --- a/apps/bot/README.md +++ b/apps/bot/README.md @@ -1,3 +1,3 @@ # apps/bot -Planned Telegram admin bot runtime. MVP should use long polling unless deployment requirements change. +Telegram admin bot runtime. The current deployment uses long polling unless requirements change. diff --git a/docs/ops/deployment.md b/docs/ops/deployment.md index a371589..09ce314 100644 --- a/docs/ops/deployment.md +++ b/docs/ops/deployment.md @@ -6,7 +6,7 @@ Deploy on one VPS with Docker Compose. ## Why this target - The system has multiple long-lived components: web, worker, bot, database, and reverse proxy. - Compose gives predictable service boundaries, easier upgrades, and easier recovery than manually managed host processes. -- It keeps the path open for later separation of web, worker, and bot without reworking the repository layout. +- It preserves clear service boundaries if separation is ever needed later. ## Expected services - `migrate`: one-shot schema bootstrap job run before app services start @@ -22,7 +22,7 @@ Deploy on one VPS with Docker Compose. - Keep persistent data in named volumes or external storage. - Keep secrets in server-side environment files or a secret manager. - Back up PostgreSQL and object storage separately. -- Prefer Telegram long polling in MVP to avoid an extra public webhook surface for the bot. +- Prefer Telegram long polling to avoid an extra public webhook surface for the bot. ## Upgrade strategy - Build new images. @@ -33,7 +33,7 @@ Deploy on one VPS with Docker Compose. ## Current database bootstrap state - The current Compose template runs a `migrate` service before `web`, `worker`, and `bot`. - The job runs `prisma migrate deploy` from the committed migration history. -- The same bootstrap job also ensures the default MVP `SubscriptionPlan` row exists after migrations. +- The same bootstrap job also ensures the default `SubscriptionPlan` row exists after migrations. - Schema changes must land with a new committed Prisma migration before deployment. ## Initial operational checklist diff --git a/docs/plan/mvp-system-plan.md b/docs/plan/system-plan.md similarity index 90% rename from docs/plan/mvp-system-plan.md rename to docs/plan/system-plan.md index c10232f..afd0bfa 100644 --- a/docs/plan/mvp-system-plan.md +++ b/docs/plan/system-plan.md @@ -1,15 +1,15 @@ -# MVP System Plan +# System Plan ## Summary Build `nproxy`, a B2C web product for image generation through external model APIs. The first model is `nano_banana`. Users register with `email + password`, pay a monthly crypto subscription, receive a monthly request limit, and use a chat-style interface for `text-to-image` and `image-to-image` generation. The service hides provider-key failures behind a routed key pool. A user request is attempted against one provider key at a time. Retryable failures move execution to the next eligible key. The user sees an error only after all eligible keys have been exhausted or the request fails for a terminal user-caused reason. -## Confirmed MVP decisions +## Confirmed product decisions - One B2C website. - One monthly subscription plan. - Crypto checkout through a payment processor. -- Manual renewal in MVP. +- Manual renewal. - Text-to-image and image-to-image. - User-facing synchronous experience implemented with polling over background execution. - Approximate quota buckets only: `100/80/60/40/20/0`. @@ -45,7 +45,8 @@ The service hides provider-key failures behind a routed key pool. A user request - notifications ## Billing rules -- One active plan in MVP. +- One active plan. +- Access ends immediately when `currentPeriodEnd` is reached. Warnings may be sent beforehand, but there is no grace period after expiry. - Each user has an individual billing cycle based on successful activation timestamp. - Limit resets on each successful cycle activation. - One successful generation consumes one request. @@ -60,7 +61,7 @@ Backend tracks exact usage. Normal users see only an approximate bucket: - `1-20%` remaining -> `20%` - `0%` remaining -> `0%` -## Generation controls in MVP +## Generation controls - mode: `text_to_image` or `image_to_image` - resolution preset - batch size @@ -94,8 +95,8 @@ Single VPS with Docker Compose, expected services: - `caddy` or `nginx` - optional `minio` when object storage is self-hosted -## Future-compatible boundaries -The codebase should be able to add later: +## Optional extensions +These are not current commitments. Consider them only if user demand, operational need, and product scale justify the added complexity: - more image providers - more billing methods - more subscription plans diff --git a/infra/caddy/Caddyfile b/infra/caddy/Caddyfile index 5e6fe28..ce7f54d 100644 --- a/infra/caddy/Caddyfile +++ b/infra/caddy/Caddyfile @@ -1,4 +1,4 @@ -# Placeholder reverse-proxy config for the future Docker Compose deployment. +# Placeholder reverse-proxy config for the Docker Compose deployment. # Replace example.com and upstream targets during implementation. example.com { diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 9ca7ed1..6977a5f 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -153,7 +153,7 @@ function readTelegramMode(env: NodeJS.ProcessEnv, key: string): "polling" { const value = readString(env, key); if (value !== "polling") { - throw new Error(`Environment variable ${key} must be "polling" for MVP`); + throw new Error(`Environment variable ${key} must be "polling"`); } return value; diff --git a/packages/db/README.md b/packages/db/README.md index 8e5c4a7..36e542a 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -4,7 +4,7 @@ Database package for `nproxy`. ## Implemented in this iteration - Prisma package scaffold -- Initial Prisma schema for MVP persisted state +- Current Prisma schema for persisted state - Shared schema path export for runtime tooling ## Current scope diff --git a/packages/db/src/auth-store.ts b/packages/db/src/auth-store.ts index 237fa2c..538ca99 100644 --- a/packages/db/src/auth-store.ts +++ b/packages/db/src/auth-store.ts @@ -73,7 +73,7 @@ export function createPrismaAuthStore(database: PrismaClient = defaultPrisma) { return database.$transaction(async (transaction) => { const defaultPlan = await transaction.subscriptionPlan.findFirst({ where: { - code: "mvp_monthly", + code: "monthly", isActive: true, }, }); diff --git a/packages/db/src/bootstrap.ts b/packages/db/src/bootstrap.ts index 81d2082..d29b31e 100644 --- a/packages/db/src/bootstrap.ts +++ b/packages/db/src/bootstrap.ts @@ -10,8 +10,8 @@ export interface SubscriptionPlanSeedInput { } export const defaultSubscriptionPlanSeed: SubscriptionPlanSeedInput = { - code: "mvp_monthly", - displayName: "MVP Monthly", + code: "monthly", + displayName: "Monthly", monthlyRequestLimit: 100, monthlyPriceUsd: 9.99, billingCurrency: "USDT", @@ -21,6 +21,8 @@ export async function ensureSubscriptionPlan( input: SubscriptionPlanSeedInput, database: PrismaClient = defaultPrisma, ): Promise { + await reconcileDefaultSubscriptionPlan(input, database); + await database.subscriptionPlan.upsert({ where: { code: input.code, @@ -48,3 +50,47 @@ export async function ensureDefaultSubscriptionPlan( ): Promise { await ensureSubscriptionPlan(defaultSubscriptionPlanSeed, database); } + +async function reconcileDefaultSubscriptionPlan( + input: SubscriptionPlanSeedInput, + database: PrismaClient, +): Promise { + const existing = await database.subscriptionPlan.findUnique({ + where: { + code: input.code, + }, + select: { + id: true, + }, + }); + + if (existing) { + return; + } + + const candidate = await database.subscriptionPlan.findFirst({ + where: { + monthlyRequestLimit: input.monthlyRequestLimit, + monthlyPriceUsd: new Prisma.Decimal(input.monthlyPriceUsd), + billingCurrency: input.billingCurrency, + }, + orderBy: { + createdAt: "asc", + }, + }); + + if (!candidate) { + return; + } + + await database.subscriptionPlan.update({ + where: { + id: candidate.id, + }, + data: { + code: input.code, + displayName: input.displayName, + isActive: true, + }, + }); +} -- 2.49.1