chore: remove MVP positioning and align plan defaults

This commit is contained in:
sirily
2026-03-10 14:58:01 +03:00
parent ba029d8f3f
commit bec00d2c9b
11 changed files with 73 additions and 26 deletions

View File

@@ -5,7 +5,7 @@ This file governs the whole repository unless a deeper `AGENTS.md` overrides it.
## Source of truth ## Source of truth
Read these files before making architectural changes: 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/system-overview.md`
- `docs/architecture/repository-layout.md` - `docs/architecture/repository-layout.md`
- `docs/ops/deployment.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. - Keep provider-specific HTTP code out of domain services.
- Model user request attempts separately from provider-key attempts. - Model user request attempts separately from provider-key attempts.
- Preserve the chosen deployment target: single VPS with Docker Compose. - 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. - Preserve the chosen quota display contract: user-facing approximate buckets only.
## Required follow-up when changing key areas ## Required follow-up when changing key areas

View File

@@ -182,7 +182,7 @@
## Полезные файлы ## Полезные файлы
- `AGENTS.md` - `AGENTS.md`
- `docs/plan/mvp-system-plan.md` - `docs/plan/system-plan.md`
- `docs/architecture/system-overview.md` - `docs/architecture/system-overview.md`
- `docs/ops/deployment.md` - `docs/ops/deployment.md`
- `docs/ops/provider-key-pool.md` - `docs/ops/provider-key-pool.md`

View File

@@ -1,12 +1,12 @@
# nproxy # 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 repository contains:
- the agreed MVP plan; - runtime applications and shared packages;
- the repository layout for future implementation; - the agreed system plan and architecture documents;
- operational notes for deployment, Telegram pairing, and provider key rotation; - 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 ## Chosen baseline
- Product: B2C website - Product: B2C website
@@ -25,7 +25,7 @@ The repository is intentionally light on runtime code. Its current purpose is to
- `scripts/` operational helpers - `scripts/` operational helpers
## Read first ## Read first
- `docs/plan/mvp-system-plan.md` - `docs/plan/system-plan.md`
- `docs/architecture/system-overview.md` - `docs/architecture/system-overview.md`
- `docs/ops/deployment.md` - `docs/ops/deployment.md`
- `CONTRIBUTING.md` - `CONTRIBUTING.md`

View File

@@ -1,3 +1,3 @@
# apps/bot # 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.

View File

@@ -6,7 +6,7 @@ Deploy on one VPS with Docker Compose.
## Why this target ## Why this target
- The system has multiple long-lived components: web, worker, bot, database, and reverse proxy. - 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. - 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 ## Expected services
- `migrate`: one-shot schema bootstrap job run before app services start - `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 persistent data in named volumes or external storage.
- Keep secrets in server-side environment files or a secret manager. - Keep secrets in server-side environment files or a secret manager.
- Back up PostgreSQL and object storage separately. - 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 ## Upgrade strategy
- Build new images. - Build new images.
@@ -33,7 +33,7 @@ Deploy on one VPS with Docker Compose.
## Current database bootstrap state ## Current database bootstrap state
- The current Compose template runs a `migrate` service before `web`, `worker`, and `bot`. - 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 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. - Schema changes must land with a new committed Prisma migration before deployment.
## Initial operational checklist ## Initial operational checklist

View File

@@ -1,15 +1,15 @@
# MVP System Plan # System Plan
## Summary ## 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. 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. 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 B2C website.
- One monthly subscription plan. - One monthly subscription plan.
- Crypto checkout through a payment processor. - Crypto checkout through a payment processor.
- Manual renewal in MVP. - Manual renewal.
- Text-to-image and image-to-image. - Text-to-image and image-to-image.
- User-facing synchronous experience implemented with polling over background execution. - User-facing synchronous experience implemented with polling over background execution.
- Approximate quota buckets only: `100/80/60/40/20/0`. - 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 - notifications
## Billing rules ## 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. - Each user has an individual billing cycle based on successful activation timestamp.
- Limit resets on each successful cycle activation. - Limit resets on each successful cycle activation.
- One successful generation consumes one request. - 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%` - `1-20%` remaining -> `20%`
- `0%` remaining -> `0%` - `0%` remaining -> `0%`
## Generation controls in MVP ## Generation controls
- mode: `text_to_image` or `image_to_image` - mode: `text_to_image` or `image_to_image`
- resolution preset - resolution preset
- batch size - batch size
@@ -94,8 +95,8 @@ Single VPS with Docker Compose, expected services:
- `caddy` or `nginx` - `caddy` or `nginx`
- optional `minio` when object storage is self-hosted - optional `minio` when object storage is self-hosted
## Future-compatible boundaries ## Optional extensions
The codebase should be able to add later: These are not current commitments. Consider them only if user demand, operational need, and product scale justify the added complexity:
- more image providers - more image providers
- more billing methods - more billing methods
- more subscription plans - more subscription plans

View File

@@ -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. # Replace example.com and upstream targets during implementation.
example.com { example.com {

View File

@@ -153,7 +153,7 @@ function readTelegramMode(env: NodeJS.ProcessEnv, key: string): "polling" {
const value = readString(env, key); const value = readString(env, key);
if (value !== "polling") { 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; return value;

View File

@@ -4,7 +4,7 @@ Database package for `nproxy`.
## Implemented in this iteration ## Implemented in this iteration
- Prisma package scaffold - Prisma package scaffold
- Initial Prisma schema for MVP persisted state - Current Prisma schema for persisted state
- Shared schema path export for runtime tooling - Shared schema path export for runtime tooling
## Current scope ## Current scope

View File

@@ -73,7 +73,7 @@ export function createPrismaAuthStore(database: PrismaClient = defaultPrisma) {
return database.$transaction(async (transaction) => { return database.$transaction(async (transaction) => {
const defaultPlan = await transaction.subscriptionPlan.findFirst({ const defaultPlan = await transaction.subscriptionPlan.findFirst({
where: { where: {
code: "mvp_monthly", code: "monthly",
isActive: true, isActive: true,
}, },
}); });

View File

@@ -10,8 +10,8 @@ export interface SubscriptionPlanSeedInput {
} }
export const defaultSubscriptionPlanSeed: SubscriptionPlanSeedInput = { export const defaultSubscriptionPlanSeed: SubscriptionPlanSeedInput = {
code: "mvp_monthly", code: "monthly",
displayName: "MVP Monthly", displayName: "Monthly",
monthlyRequestLimit: 100, monthlyRequestLimit: 100,
monthlyPriceUsd: 9.99, monthlyPriceUsd: 9.99,
billingCurrency: "USDT", billingCurrency: "USDT",
@@ -21,6 +21,8 @@ export async function ensureSubscriptionPlan(
input: SubscriptionPlanSeedInput, input: SubscriptionPlanSeedInput,
database: PrismaClient = defaultPrisma, database: PrismaClient = defaultPrisma,
): Promise<void> { ): Promise<void> {
await reconcileDefaultSubscriptionPlan(input, database);
await database.subscriptionPlan.upsert({ await database.subscriptionPlan.upsert({
where: { where: {
code: input.code, code: input.code,
@@ -48,3 +50,47 @@ export async function ensureDefaultSubscriptionPlan(
): Promise<void> { ): Promise<void> {
await ensureSubscriptionPlan(defaultSubscriptionPlanSeed, database); await ensureSubscriptionPlan(defaultSubscriptionPlanSeed, database);
} }
async function reconcileDefaultSubscriptionPlan(
input: SubscriptionPlanSeedInput,
database: PrismaClient,
): Promise<void> {
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,
},
});
}