Welcome to Zenska.ph
backend development
This handbook covers everything from local setup to AWS migration. Read it once, refer to it daily. If something isn't here, ask a senior before guessing.
Zenska.ph is a trust-first multi-vendor beauty marketplace in the Philippines. Only verified, authorized sellers are allowed. Customers get AI-powered skin analysis to find the right products. We support COD (cash on delivery), GCash, and card payments with pickup logistics via J&T and Ninja Van. Our moat is trust — Shopee and Lazada cannot become curated marketplaces. We can, and we are.
- WordPress + WooCommerce on Hostinger — product catalog, vendor listings
- Dokan plugin — vendor dashboards, COD tagging per product
- Vendor onboarding already active
- AWS S3 — product images and videos already stored here
- Cloudflare — DNS and CDN in front of Hostinger
- Node.js API on Railway — shared backend for web + future mobile
- PostgreSQL on Railway — our own database, replaces WC DB eventually
- Redis on Railway — sessions, cart state, rate limiting
- Typesense Cloud — fast product search replacing WordPress search
- AI skin service — separate Railway service, quiz + ingredient matching
AI-assisted development
timeline
Using Claude and GitHub Copilot aggressively cuts development time by 40–60%. Here's exactly what that looks like for a 4-person team building the Zenska backend.
- For boilerplate: "Write a Fastify route handler for POST /api/orders that validates with Zod, checks COD eligibility, creates a Prisma record, and returns our standard {success, data} response format."
- For integrations: "Write a Node.js function that calls the PayMongo API to create a GCash payment intent. Use axios. Include error handling and return the checkout URL."
- For debugging: Paste the exact error + the function code + "What is wrong and how do I fix it?"
- For schema design: "Design a PostgreSQL schema for a multi-vendor marketplace with users, vendors, products, orders, order_items, and settlements. Use Prisma schema syntax."
- For testing: "Write Jest unit tests for this COD eligibility function. Test all edge cases: total over limit, non-COD items, new account limit, and happy path."
- Write the function name and JSDoc comment first — Copilot reads your comment and generates the body. The more descriptive your comment, the better the output.
- Tab-complete repetitive patterns — once you write one Fastify route, Copilot generates the next one from the pattern. Perfect for bulk endpoint creation.
- Use Copilot Chat for inline explanations — highlight unfamiliar code, ask "explain this". Faster than Google for library-specific questions.
- Never accept without reading — Copilot gets Prisma queries wrong about 30% of the time. Always read before pressing Tab, especially for database operations.
- Don't use Copilot for security-critical code — JWT verification, payment webhook signature checking, and COD fraud logic must be written manually and reviewed by a senior.
Development phases
Three phases. The website stays live throughout. We build alongside it and migrate gradually. Customers never experience downtime.
Project setup
Follow these steps exactly on day one. Don't skip steps or reorder them.
main. Not juniors, not seniors. Every change goes through a Pull Request. Main branch = production.Railway &
databases
Railway hosts our Node.js backend, PostgreSQL, and Redis. Setup takes 30 minutes. Here's the complete walkthrough.
- 1Go to railway.app → click New Project → Deploy from GitHub repo → select zenska/backend
- 2Click + New → Database → Add PostgreSQL. Railway provisions Postgres 15 in ~10 seconds. The
DATABASE_URLvariable is auto-injected into your service. - 3Click + New → Database → Add Redis. Same process —
REDIS_URLauto-injected. - 4Go to your Node.js service → Variables tab → add all remaining env vars (JWT_SECRET, TYPESENSE keys, PayMongo keys, etc.)
- 5Every push to main branch auto-deploys in ~60 seconds. Watch the Deploy tab for build logs.
Connecting services
from your codebase
This chapter shows exactly how PostgreSQL, Redis, and Typesense connect from your Node.js code to Railway and Typesense Cloud. Every connection pattern, every config file, every gotcha.
DATABASE_URL and REDIS_URL environment variables that Railway auto-injects. You never hardcode these. You never need to know the IP address. Railway handles it.Folder structure
One codebase, clean domain separation. Each folder owns one business area. Nothing crosses boundaries without going through the API layer.
PrismaClient(), Redis(), or Typesense.Client() inside a module file. Always import from lib/. Creating multiple instances causes connection pool exhaustion — a production crash that is very hard to debug.Typesense search
Replaces WordPress search with instant, typo-tolerant, filterable product search. Customers filter by COD, skin type, price, and brand — all in under 50ms.
- 1Go to cloud.typesense.org → create account → New Cluster → choose Singapore region
- 2When cluster is ready, go to API Keys → copy the Admin API Key and the Search-Only API Key. Use Admin key server-side only. Use Search-Only key if ever exposing search to browser JS.
- 3Copy Host (looks like xxx.a1.typesense.net), Port (443), Protocol (https) → paste all into Railway Variables
- 4Your collection is created automatically on first app start via the
ensureCollection()function in lib/typesense.js
The full sync code is in the Connecting services chapter. Schedule it in app.js:
Add this JavaScript to your WordPress theme's functions.php or a custom plugin. No WordPress plugin needed — it's a pure JS override of the search form.
Postman &
API documentation
Postman is the contract between backend and everyone else. An endpoint is not done until it has a Postman entry with a working example and documented response.
- 1Go to postman.com → New Team Workspace → name it "Zenska API"
- 2Invite all 4 developers. Seniors can edit. Juniors can view and run.
- 3Create a Collection "Zenska Backend v1" with folders: Auth / Products / Search / Orders / Payments / Skin / Vendors / Admin
- 4Create 2 Environments: LOCAL (base_url=http://localhost:3000) and RAILWAY (base_url=https://yourapp.up.railway.app)
In all other requests, set Authorization header to: Bearer {{jwt_token}}
Moving off
WordPress
Phase 2 migrates the web frontend from WooCommerce to our Node.js API — one section at a time. The website never goes down.
/api/search. Search box looks identical. Results come from Typesense via Node.js. WooCommerce product pages still linked./api/orders and /api/payments. Most complex migration. Requires full QA before switching.COD &
payments
COD will be 50–70% of Zenska orders. The validation logic must be bulletproof and server-enforced from day one.
- Run COD eligibility check server-side
- Create order in Postgres, status = PENDING
- Call J&T / Ninja Van API for pickup booking
- Send SMS to customer via Semaphore
- Logistics partner collects cash on delivery
- Settlement runs after 3PL confirms delivery
- Call PayMongo API to create payment intent
- Return checkout URL to frontend
- Customer completes on PayMongo hosted page
- PayMongo calls our
/api/payments/webhook - Webhook verifies signature → updates order to CONFIRMED
- Send SMS + email confirmation to customer
Why Railway,
not AWS yet
A deliberate decision based on your team size and current stage. Here's the complete reasoning.
AWS migration
when you're ready
Two focused weekends. One senior developer. Zero app code changes. Only environment variables change.
- Railway bill consistently above $150/month
- Database exceeds 8GB
- API p95 response time above 400ms
- You need read replicas for analytics
- GMV consistently above ₱5M/month
- You hire a DevOps or senior infra engineer
- You need compliance or data residency in PH
Roles &
non-negotiable rules
Four developers, clear ownership. No ambiguity about who builds what.
- Node.js architecture, Fastify setup, Railway CI/CD
- PostgreSQL schema — owns all Prisma migrations
- Auth system (JWT, OTP, refresh tokens)
- Order service, COD validation, multi-vendor split
- PayMongo integration, webhook handling
- Reviews all backend PRs before merge
- WordPress JS integration layer (search, skin, checkout)
- React Native architecture and component library (Phase 3)
- Typesense search UI integration
- Skin analyzer quiz flow UI
- Reviews all frontend / mobile PRs before merge
- WooCommerce product setup, Dokan COD tagging
- Vendor onboarding flow, product CSV import
- Owns and maintains Postman collection
- QA testing — all flows 1–5 from flow document
- S3 upload integration (supervised by S1)
- AI skin service — separate Railway deployment
- Ingredient compatibility table seeding
- J&T and Ninja Van logistics API integration
- Twilio/Semaphore SMS, Resend email notifications
- QA testing — flows 6–10, unit tests for AI logic
Error tracking &
observability
Right now your production monitoring plan is "wait for customers to complain." That is not a plan. This chapter fixes it. Total setup time: one day. Cost: free.
SENTRY_DSN=https://abc123@o123.ingest.sentry.io/456- 1Go to uptimerobot.com → free account → Add New Monitor
- 2Monitor type: HTTP(s) → URL:
https://your-api.railway.app/health→ interval: 5 minutes - 3Add alert contacts: email addresses for both seniors. Optional: add a Slack webhook for the dev channel.
- 4Add a second monitor for the main website:
https://zenska.ph - 5Add a third monitor for the AI service:
https://your-ai-service.railway.app/health
COD fraud detection
& management
COD fraud will hit Zenska within the first two months of live orders. Failed deliveries, fake addresses, and repeat refusers cost real money. This system catches them before they accumulate.
- Fraud signals feed — real-time list of new signals with risk score, entity, and signal type. Seniors review daily.
- Blocked entities list — all currently blocked phones, addresses, users. With unblock button requiring reason.
- Review queue — orders with decision=REVIEW awaiting manual approval or cancellation. Target: zero items in queue by end of each business day.
- Refusal rate by address zone — barangay-level refusal heatmap. High-refusal zones get automatic lower COD limits.
- Weekly fraud report — total blocked orders, estimated losses prevented, false positive rate (legitimate orders flagged).
Vendor dashboard
essential features
The vendor dashboard is where Zenska's trust promise is either kept or broken. If vendors cannot see their orders clearly, get paid transparently, and manage their store without confusion — they leave. These are the features that matter.
- Today's stats: orders today, revenue today, pending pickups, items to ship
- Pending actions: orders awaiting confirmation, low stock alerts, unread messages
- Recent orders: last 5 orders with status pill (Pending / Confirmed / Shipped / Delivered)
- Settlement due: amount expected in next payout cycle with date
- Order list with filters: All / Pending / Confirmed / Shipped / Delivered / Cancelled
- Order detail: customer name (not phone for privacy), items ordered, full delivery address, payment method (COD/GCash/Card), order total after commission
- One-click actions: Confirm Order → triggers pickup booking with 3PL + SMS to customer. Mark as Packed. Request pickup.
- Tracking number: shows 3PL tracking number once pickup is booked. Clickable link to courier tracking page.
- Vendors must NOT see customer phone numbers directly — only Zenska support can share this if needed.
- Product list with live/draft/out-of-stock status. Search and category filter.
- COD toggle per product — vendor ticks whether each product is COD eligible. This feeds into our COD validation logic. Default: off. Vendor opts in per product.
- Stock management — update stock count. Alert badge when stock < 5 units.
- Image upload — drag-and-drop to S3 via pre-signed URL. Max 5 images per product. Auto-optimized via Cloudflare Images.
- Bulk CSV upload — for vendors with large catalogs. Template downloadable. Junior Dev 1 builds this in Sprint 2.
- Settlement summary: current cycle total, commission deducted, gateway fees (at actual cost, no markup), net payout amount
- Settlement history: every past payout with date, amount, and breakdown. Downloadable as PDF/CSV for BIR.
- Per-order breakdown: expandable row showing each order's gross → commission (8%) → net for that order
- Next payout date prominently displayed. Settlement cycle: T+7 after delivery confirmed.
- No payout is released until the delivery is confirmed by the 3PL webhook. This is automatic — no manual step.
- Revenue chart: daily/weekly/monthly gross revenue. Line chart. 30 and 90 day views.
- Top products: best-selling products by order count and by revenue. Helps vendors know what to restock.
- Return rate: % of orders returned or refused. Colour-coded: green <5%, amber 5–10%, red >10%.
- COD vs online split: pie chart of payment methods. Helps vendors decide COD eligibility strategy.
- Keep it simple at launch. Vendors don't need 40 charts. They need 4 clear numbers.
- Store profile: store name, description, logo upload (S3), banner image
- Bank account: bank name, account number, account name for settlement payout. Required before first payout. Verified by Zenska admin.
- Business docs: view verification status of submitted documents. Upload new docs if requested by admin.
- Notification preferences: which SMS/email alerts to receive (new order, low stock, settlement, etc.)
- Vendors cannot change their own verification status or bank account without admin approval. Fraud prevention.
vendorId = req.vendor.id./api/upload/presign with their vendor ID in the key path.req.vendor with verified status. Any vendor whose status is not active should get 403 on all write operations.