chore: remove MVP positioning and align plan defaults
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -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`
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user