Minbook
KO
Prototype to Production — The Complete Change List

Prototype to Production — The Complete Change List

MJ · · 13 min read

10 key changes for transitioning an MVP to commercial SaaS: security hardening, JWT auth, KO/EN i18n, and payment automation to build a 'payment-ready' service.

Overview

The WICHI MVP, built in 3 days at a hackathon, was in a state of “it works, but it’s not ready to charge money.” The core analysis functionality ran, and the signup-payment-analysis-report flow was connected. But transitioning to a service that actually accepts payments required filling a substantial number of gaps.

This post catalogs the changes made during the transition from prototype to commercial service, organized by category. For each item, I document the prototype state, what changed, and why. It intentionally excludes specific implementation code or cost figures. The focus is on what changed and the reasoning behind each change.

Full Change Category List

#CategoryPrototype StateProduction StatePriority
1Security hardeningBasic input limits onlyMulti-layer validation + concurrency limitsP0
2AuthenticationBasic session-basedJWT + token refreshP0
3Internationalization (i18n)English onlyKO/EN dual languageP0
4Sample systemNoneLocale-specific sample reportsP1
5Payment integrationSemi-manualWebhook automation + credit systemP0
6Error handling & monitoringconsole.log levelStructured errors + GA4P1
7DB schemaMinimal table structureNormalization + indexingP1
8Frontend hostingNo-code builderVercel static hostingP0
9DNS / SSL / custom domainBuilder default domainCustom domain + SSLP0
10SEO / OG imagesNoneSearch Console + meta + OGP1
graph LR
    subgraph "Prototype"
        A[FE: No-code Builder] --> B[BE: Basic API]
        B --> C[DB: Minimal Schema]
        B --> D[LLM API]
    end

    subgraph "Production"
        E[FE: Vercel + React] --> F[BE: FastAPI + JWT + Rate Limit]
        F --> G[DB: Normalized + Indexed]
        F --> H[LLM API]
        F --> I[Payment Webhooks]
        F --> J[GA4 Events]
        E --> K[i18n KO/EN]
        E --> L[Sample System]
        E --> M[SEO + OG]
    end

1. Security Hardening

Prototype State

Only frontend input length limits were in place. The backend had no separate validation logic and processed request bodies as-is. There was no defensive code assuming malicious input. For a hackathon MVP, this was sufficient — the users were a handful of judges with no motivation for malicious use.

Changes

Security hardening was applied across three layers.

Layer 1: Frontend UX Validation

  • Input format validation (URL patterns, email format, etc.)
  • Input length limits (min/max)
  • Immediate guidance messages for special characters and disallowed strings
  • Clear conditions for submit button deactivation

Frontend validation is UX, not security. Its role is to explain to users “why this input isn’t accepted.” Actual defense happens on the backend.

Layer 2: Backend Allowlist Validation

  • Allowlist-based validation that only permits known-good values
  • Enum-type parameters compared against a defined value list
  • String parameters go through pattern matching before being accepted
  • Disallowed values result in immediate request rejection (400 response)

Why allowlist over denylist: A denylist blocks only “known bad inputs,” leaving it vulnerable to novel attack patterns. An allowlist passes only “known good inputs,” making it safe by default.

Layer 3: Forbidden Pattern Filtering

  • Prompt injection attempt detection and blocking
  • System prompt exfiltration attempt blocking
  • Filtering of unintended special character combinations
  • Forbidden pattern list managed in a separate config file (not hardcoded)

Additional Defense: Concurrency Limits

  • Per-user cap on concurrent analysis requests
  • Queue notification or request rejection when limit is exceeded
  • Credit deduction is only finalized after successful analysis (recovered on failure)
LayerLocationRoleBlocking Point
1FrontendUX guidanceAt input time
2Backend entryAllowlist validationOn request receipt
3Backend processingForbidden pattern detectionBefore processing
AdditionalBackendConcurrency limitsAt execution time
graph TD
    A[User Input] --> B{FE Format Validation}
    B -->|Pass| C[API Request]
    B -->|Fail| D[Input Error Guidance]
    C --> E{BE Allowlist Validation}
    E -->|Pass| F{Forbidden Pattern Filter}
    E -->|Fail| G[400 Bad Request]
    F -->|Pass| H{Concurrency Limit Check}
    F -->|Fail| I[403 Forbidden]
    H -->|Available| J[Execute Analysis]
    H -->|Exceeded| K[429 Too Many Requests]
    J --> L{Analysis Successful?}
    L -->|Success| M[Finalize Credit Deduction]
    L -->|Failure| N[Recover Credits]

Why This Changed

The moment payments are attached, malicious use has financial motivation. With a free MVP, someone entering garbage input causes minimal damage. With a paid service, credit abuse, API exploitation, and prompt injection translate directly into costs. Each LLM API call has a per-call cost, so malicious use directly increases operating expenses.

Security is not “something to do later” — it becomes mandatory the moment payment functionality is added.


2. Authentication

Prototype State

Basic session-based authentication was in use. Sessions persisted after login with no consideration for token expiration or refresh. CORS was set to allow everything (*) for development convenience.

Changes

JWT-Based Authentication

  • Separated access tokens and refresh tokens
  • Short expiration time on access tokens
  • Automatic refresh via refresh tokens
  • User role and permission information included in token payload

CORS Hardening

  • Removed wildcard (*) permission
  • Explicitly specified allowed domains (production domain, preview domain)
  • Explicitly restricted allowed methods and headers
  • Enabled Credentials option

Rate Limiting

  • Dual-layer limiting: IP-based + user-based
  • Per-endpoint differentiated limits (authentication endpoints are stricter)
  • 429 response on limit breach with retry-after timing
ItemPrototypeProduction
Auth methodSession-basedJWT (access + refresh)
Token expirationNot consideredShort expiry + auto-refresh
CORS* (allow all)Explicit domain specification
Rate limitNoneDual-layer IP + user
Permission managementNoneRoles included in token payload

Why This Changed

Session-based authentication is simple in a single-server environment, but has limitations in a SaaS architecture where frontend and backend run on different domains. JWT is stateless, making it favorable for server scaling, and allows the frontend to manage tokens directly — well-suited for SPA architectures. CORS and rate limiting are baseline defenses for any service exposing an API externally.


3. Internationalization (i18n)

Prototype State

English only. All UI text was written directly in component code. Button text, guidance messages, and error messages were hardcoded as English strings.

Changes

Frontend Changes

  • Separated UI text from code into per-locale JSON files
  • Added language toggle UI (KO/EN toggle in header)
  • Automatic language detection based on browser locale
  • Manual selection saved to local storage
  • Date and number formatting adapted per locale
  • Locale prefix applied to links and routing paths

Backend Changes

  • Analysis report generation branches prompts based on request locale
  • System prompts sent to the LLM include language instructions
  • API error messages branch by locale
  • Email notification templates separated by locale

Translation Management

  • Established naming conventions for translation keys (e.g., page.analysis.button.start)
  • Fallback to English when a key is missing
  • Maintained a translation file update checklist
AreaPrototypeProduction
UI textHardcoded in componentsSeparated into per-locale JSON
Report contentEnglish onlyBranches by request locale
Error messagesEnglish onlyPer-locale branching
Date/number formattingUS format onlyLocale-based formatting
RoutingSingle pathLocale prefix
DetectionNoneBrowser locale auto-detection

Why This Changed

WICHI is a global SaaS. To enter the Korean market and expand globally at the same time, i18n is not optional — it’s mandatory.

Adding i18n later means ripping apart every UI component. The work of finding hardcoded strings one by one and replacing them with translation keys grows exponentially as the component count increases. The commercialization milestone was the most efficient time to establish the structure.

i18n is a “structure” problem, not a “translation” problem. Separating text from code is the core task — actual translation comes after.

Backend i18n has more considerations than frontend. In an LLM-based service, the analysis output itself must change language, requiring language instructions at the prompt level. This is a fundamentally different scope of work from simply translating UI.


4. Sample System

Prototype State

No sample data existed. Users had to spend credits after signing up to see any analysis results. The structure required payment before users could verify “what kind of results does this service produce?”

Changes

Sample Report System

  • Sample reports accessible without login
  • English samples: based on a global DTC brand
  • Korean samples: based on a major Korean brand
  • Sample reports rendered using the same UI as real analysis results

Conversion Funnel Design

  • Signup gate placed at the end of sample reports
  • Only a certain percentage of the full report is viewable for free
  • Gate messaging branches by locale
  • GA4 tracking on sample-to-signup conversion events

Sample Selection Criteria

  • Selected brands with high recognition for the relevant locale’s users
  • Selected brands that produce rich analysis results (thin results have the opposite effect)
  • Periodic sample data refresh (outdated data undermines credibility)
ItemPrototypeProduction
Output previewNot possibleAvailable via sample reports
Pre-login accessNot possibleAvailable for samples
Locale-specific contentN/AMarket-appropriate brands per locale
Conversion funnelNoneSignup gate + GA4 events

Why This Changed

“I want to see what I’m getting before I pay” is a natural impulse. Especially with a SaaS delivering “analysis reports” — a text-based output — there’s an inherent perception that quality can vary widely. Requiring payment first, with no samples, makes conversion nearly impossible.

The reason for locale-specific brand samples is equally clear. Showing US brand samples to a Korean user leaves the question “does this even apply to our market?” You need to show analysis results for brands that users in each market personally recognize to convey the value of the output.

Samples are not “demos” — they’re “evidence.” They prove to users that “this service delivers results worth paying for.”


5. Payment Integration

Prototype State

A payment page existed, but webhook handling was incomplete. Code to receive payment success events was in place, but there was no handling for edge cases — duplicate events, retry after failure, refunds. Credit allocation was done manually.

Changes

Webhook Automation

  • Receive and verify webhook event signatures from the payment platform
  • Route to handlers by event type (payment complete, refund, subscription change, etc.)
  • Idempotency guarantee — duplicate receipt of the same event is processed only once
  • Retry resilience — duplicate prevention even on retries after receipt failure
  • All received webhook events logged (for debugging and audit trail)

Credit System Design

  • Three-stage flow: payment complete → credit charge → deduction on analysis
  • Credit balance query API
  • Credit deduction is reserved at analysis start, confirmed on completion
  • Automatic recovery of reserved credits on analysis failure
  • Credit usage history (charge, deduction, recovery records)

Order State Management

  • Order state machine: payment pending → payment complete → credits granted → (on refund) credits recovered
  • Timestamp recorded for each state transition
  • Alerts on abnormal state transitions
stateDiagram-v2
    [*] --> PaymentPending: Enter payment page
    PaymentPending --> PaymentComplete: Webhook received (payment success)
    PaymentPending --> PaymentFailed: Webhook received (payment failure)
    PaymentComplete --> CreditsGranted: Credit charge processed
    CreditsGranted --> [*]
    PaymentComplete --> RefundRequested: User requests refund
    RefundRequested --> CreditsRecovered: Check remaining credits and recover
    CreditsRecovered --> RefundComplete: Refund processing complete
    RefundComplete --> [*]
    PaymentFailed --> [*]

Defensive Logic

  • Server-side re-verification of payment amount and product info (client-submitted values are never trusted)
  • Detection and blocking of duplicate payments from the same user in rapid succession
  • Constraints preventing credit balance from going negative

Why This Changed

In an MVP, “payments kind of work” is sufficient. In a commercial service, it’s not. If credits aren’t reflected immediately after payment, users experience “I paid money but can’t use the service.” If duplicate deductions occur, trust is destroyed.

Webhook-based automation and transaction safety are baseline requirements for any paid service. The reserve-confirm pattern for credit deduction was especially essential since analysis depends on LLM API calls, which always carry a possibility of failure.

In a payment system, “it works most of the time” is not good enough. A single edge case is directly tied to a user’s money.


6. Error Handling and Monitoring

Prototype State

Error handling was at the console.log level. When errors occurred, they appeared in the browser console or as stack traces in backend logs. User-facing error messages were generic “Something went wrong” text. No user behavior data was being collected.

Changes

Frontend Error Handling

  • Error Boundaries applied — component errors are isolated so they don’t crash the entire app
  • User-friendly messages displayed on API request failure (branching by locale)
  • Differentiated guidance for network errors vs. server errors
  • Retry buttons provided for retryable errors

Backend Error Handling

  • Unified structured error response format (code, message, detail fields)
  • Accurate HTTP status code usage (previously most errors returned 500)
  • Clear error level classification: user input errors (4xx) vs. server errors (5xx)
  • Retry logic with exponential backoff for LLM API call failures
  • Analysis failure processing + credit recovery after retry exhaustion

Monitoring — GA4 Integration

  • Key conversion events defined and tracked:
EventTrigger PointPurpose
sign_upSignup completionMeasure signup conversion rate
loginLoginIdentify return visit patterns
view_sampleSample report viewMeasure sample effectiveness
begin_checkoutPayment page entryCheckout funnel start point
purchasePayment completionMeasure payment conversion rate
begin_analysisAnalysis startCredit usage patterns
analysis_completeAnalysis completionAnalysis success rate
analysis_failedAnalysis failureFailure rate monitoring

Structured Logging

  • Request ID-based tracing — a single request can be tracked across multiple services
  • Systematized log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • Sensitive data (tokens, passwords, API keys) masked in logs

Why This Changed

You can’t improve without data. You need to know where users drop off, what the payment conversion rate is, and what percentage of users who start an analysis actually complete it — only then can you set the next priorities.

Error handling is the same story. “Something went wrong” is useless to both users and developers. Users need to know “what went wrong and what they should do.” Developers need “where and why it failed” recorded in a structured format.


7. Database Schema Evolution

Prototype State

Only minimal tables existed — a users table and an analysis results table, essentially. Normalization hadn’t been done. No indexes existed beyond primary keys, and there was no consideration for query performance.

Changes

Table Additions and Normalization

  • New credit transaction table (charge, deduction, recovery history)
  • New order table (payment state machine)
  • New sample report table
  • User settings table separated (locale, notification preferences, etc.)
  • Analysis results table restructured (locale column added, metadata normalized)

Indexing

  • Indexes added for frequently accessed query patterns
  • Credit balance query optimized (user ID + creation date composite index)
  • Analysis result list query optimized (user ID + status + creation date)
  • Eliminated unnecessary full table scans

Data Integrity

  • Foreign key constraints added
  • CHECK constraint to prevent negative credit balances
  • Triggers or application-level validation for state transitions
  • Soft delete pattern applied (preventing accidental data loss)
ItemPrototypeProduction
Table count2-37+
IndexesPK onlyComposite indexes based on query patterns
Foreign keysNoneApplied to all relationships
Data integrityApplication-level onlyDB-level constraints + app-level validation
Deletion approachHard deleteSoft delete

Why This Changed

In a prototype, the goal is “data gets saved.” In a commercial service, the goal is “data is accurate, queryable fast, and not accidentally lost.”

The addition of the credit system in particular made transaction consistency critical. If credit deduction and analysis execution aren’t processed atomically, you get situations where credits are deducted but the analysis fails. DB-level constraints are the last safety net against such scenarios.


8. Frontend Hosting

Prototype State

The frontend was hosted on a no-code builder. It was fast for building UI during the hackathon, but had limits on customization and cost control at the commercialization stage.

Changes

  • Fully migrated from no-code builder to code-based frontend (React + Vite)
  • Switched to Vercel-based static hosting
  • Built git-push-based automatic deployment pipeline
  • Automatic Preview Deploy created for each PR
  • Instant rollback to previous deployment versions
  • Environment variables separated into Production / Preview / Development
  • All builder-specific dependencies completely removed
ItemNo-code BuilderVercel
Deploy triggerManual from editorgit push → automatic
PR previewNot availablePreview Deploy per PR
RollbackManual restoreInstant rollback
Environment variablesLimitedSeparated by environment
Code controlPartialFull
Monthly costPaid subscriptionFree tier

Why This Changed

Vercel’s free tier was more economical than the no-code builder’s monthly subscription. More importantly, direct control over frontend code was required to implement i18n, SEO, custom components, and other features necessary for commercialization. Prototyping tools and production hosting serve different roles.


9. DNS / SSL / Custom Domain

Prototype State

Using the default subdomain provided by the no-code builder. SSL was auto-provisioned by the builder. No custom domain was connected.

Changes

Custom Domain Setup

  • Purchased and registered a production domain
  • Configured nameserver records in Cloudflare DNS
  • Connected custom domains to both frontend (Vercel) and backend (Railway)
  • Set up www-to-root domain redirect

SSL Certificates

  • Vercel: Automatic SSL provisioning (Let’s Encrypt)
  • Backend: Railway automatic SSL
  • Certificate renewal is auto-managed on both sides

DNS Record Cleanup

  • Accurate A record and CNAME record configuration
  • Removed unnecessary legacy records
  • Set up redirect from previous domain after DNS propagation confirmation
ItemPrototypeProduction
DomainBuilder subdomainCustom domain
SSLBuilder auto-provisionAuto-provisioning on both sides
DNS managementN/ACloudflare
www redirectN/AConfigured

Why This Changed

A subdomain like something.builder-name.app is fine for a prototype, but undermines trust for a paid service. A custom domain is the baseline of brand identity, and SSL is the baseline of user data protection. For a service handling payment information, neither is negotiable.


10. SEO / OG Images

Prototype State

Zero SEO presence. No robots.txt, no sitemap.xml. Meta tags were at defaults, and Open Graph images were not configured. The service had never been registered with any search engine.

Changes

Search Engine Registration

  • Registered domain property in Google Search Console
  • Ownership verified via DNS TXT record
  • Auto-generated and submitted sitemap.xml
  • Configured robots.txt (explicitly specifying crawlable paths)

Meta Tag Optimization

  • Per-page title and description optimization
  • Added structured data (JSON-LD) with SoftwareApplication schema
  • Canonical URL configuration (preventing duplicate content)
  • Per-locale hreflang tags (multilingual search exposure)

Open Graph Images

  • Created a main service OG image
  • OG image branching by key pages
  • OG images stored within the project (removed external CDN dependency)
  • Twitter Card meta tags also configured

Technical SEO

  • Page load speed optimized (a side benefit of the static hosting migration)
  • Mobile responsiveness verified
  • Core Web Vitals baseline achieved
graph TD
    A[Search Engine Crawler] --> B[Check robots.txt]
    B --> C[Parse sitemap.xml]
    C --> D[Crawl Pages]
    D --> E[Collect Meta Tags]
    D --> F[Collect Structured Data]
    D --> G[Check hreflang]
    E --> H[Display in Search Results]
    F --> H
    G --> I[Branch Search Results by Locale]

    J[Social Share] --> K[Parse OG Meta Tags]
    K --> L[Display OG Image + Title + Description]

Why This Changed

For users to find a paid SaaS, it needs to be discoverable in search. For an early-stage SaaS starting without ad spend, organic search is effectively the only free acquisition channel. Without SEO, even the best service remains invisible.

OG images determine the first impression when links are shared socially. The difference between a cleanly displayed title, description, and image versus a blank preview directly affects click-through rates.


Prioritization Criteria

Since all 10 categories can’t be tackled simultaneously, priorities had to be set. P0 and P1 were divided using the following criteria.

P0 (Must-have before launch — minimum requirements to accept payments)

  • Security hardening: security is non-optional when payments are involved
  • Authentication: APIs cannot be protected without JWT
  • Payment integration: manual credit allocation doesn’t scale
  • Frontend hosting: i18n and SEO can’t be implemented without code control
  • DNS/SSL/domain: a paid service can’t operate on a builder subdomain
  • i18n: essential for Korean market entry; retrofitting later is exponentially more expensive

P1 (Must-have post-launch — requirements for growth)

  • Sample system: directly impacts conversion rate
  • Error handling/monitoring: improvement is impossible without data
  • DB schema refinement: data integrity and performance
  • SEO/OG images: establishing an acquisition channel
graph TD
    subgraph "P0: Required Before Payments"
        A[Security Hardening] --> B[Authentication]
        B --> C[Payment Integration]
        D[FE Hosting Migration] --> E[DNS/SSL/Domain]
        D --> F[i18n]
    end

    subgraph "P1: Required for Growth"
        G[Sample System]
        H[Error Handling/Monitoring]
        I[DB Schema Refinement]
        J[SEO/OG Images]
    end

    C --> G
    F --> G
    E --> J
    B --> H
    C --> I
PriorityCriterionCategories
P0Cannot accept payments without thisSecurity, Auth, Payments, Hosting, DNS/SSL, i18n
P1Cannot grow without thisSamples, Monitoring, DB refinement, SEO
P2Nice-to-have but launchable without(Items not covered in this post)

Full Summary

#AreaPrototypeProductionPriority
1SecurityBasic input limits3-layer validation + concurrency limits + transaction safetyP0
2AuthSession-basedJWT + CORS + Rate LimitP0
3i18nEnglish onlyKO/EN dual language (FE + BE)P0
4SamplesNoneLocale-specific sample reports + signup gateP1
5PaymentsSemi-manualWebhook automation + credit systemP0
6Errors/Monitoringconsole.logStructured errors + GA4 with 8 eventsP1
7DBMinimal tablesNormalization + indexing + integrity constraintsP1
8HostingNo-code builderVercel static hostingP0
9DNS/SSLBuilder subdomainCustom domain + auto SSLP0
10SEONoneSearch Console + sitemap + OG imagesP1

Retrospective

These 10 items were not things “missing from the MVP.” They were things that weren’t needed at the prototype stage.

The purpose of a prototype is to validate core value. The only question was “is this analysis useful?” — and for that purpose, the MVP was sufficient.

The purpose of commercialization is to deliver that value reliably, repeatedly, and trustworthily. All 10 items above fall under the latter.

A prototype answers “is this possible?” A commercial service answers “can this be trusted?”

Patterns observed during the transition:

  • The moment payments attach, the standard for everything changes. Security, error handling, data integrity — items that were “nice-to-have” when free become “must-have” when paid.
  • i18n is a structural problem. It’s about separating text, not translating. Do it when your component count is small.
  • Samples are trust devices, not marketing. Showing users “what this service delivers” is not a sales tactic — it’s basic respect for the user.
  • Without monitoring, improvement relies on intuition. You need data to set priorities.
  • DB constraints are the last line of defense. Even when application code has bugs, DB-level constraints protect data integrity.

Items not covered in this post — blog, email marketing, A/B testing, advanced analysis features, and more — will be documented separately in future posts.

Share

Related Posts