Multi-tenant subdomain-based self-service refund portal for ikas e-commerce platform
ikas e-ticaret platformu için geliştirilmiş kapsamlı iade yönetim ve self-service portal uygulaması. Next.js 15 App Router, OAuth, Prisma, GraphQL (codegen), Tailwind CSS ile modern ve güvenli bir altyapı üzerine kurulmuştur.
- Subdomain-based portals: Each merchant gets their own subdomain (e.g.,
paen.enestekin.com) - Automatic subdomain generation: OAuth onboarding automatically creates URL-safe subdomains with Turkish character support
- Wildcard domain support: Configured on Vercel with
*.enestekin.com - Middleware routing: Next.js 15 Edge middleware for subdomain detection and routing
- 10x faster portal: Response time reduced from 30+ seconds to 3-5 seconds
- Proactive token refresh: Tokens refresh 5 minutes before expiry to avoid timeouts
- Lightweight GraphQL queries: Optimized queries with only essential fields
- Retry mechanism: Automatic retry with 2-second delay for transient failures
- Rate limiting: IP-based rate limiting to prevent brute force attacks and DDoS
- Portal verification: 10 requests/minute
- Refund tracking: 20 requests/minute
- Security logging: Comprehensive logging for rate limit violations
- Rate limit headers: Standard HTTP headers (X-RateLimit-*) in responses
- Detailed changelog: Comprehensive documentation of all changes in CHANGELOG_2025_11_13.md
- Better error handling: Improved error messages and user feedback
- Vercel timeout optimization: Increased function timeout to 30 seconds
| Dosya | Açıklama |
|---|---|
| ARCHITECTURE.md | Sistem mimarisi, veri akışları, teknoloji stack ve tasarım kararları |
| DEPLOYMENT_GUIDE.md | Production deployment adımları, environment setup ve konfigürasyon |
| TROUBLESHOOTING.md | Karşılaşılan sorunlar, çözümler ve debugging ipuçları |
| CHANGELOG.md | Değişiklik geçmişi ve planlanan özellikler |
| docs/CHANGELOG_2025_11_13.md | 13 Kasım 2025 detaylı değişiklik listesi (subdomain, performance, security) |
| .docs/DEVELOPMENT_LOG.md | Detaylı geliştirme süreci notları |
- İki Sekme ile İade Yönetimi:
- ikas Siparişleri: Son 90 günde iade durumundaki siparişler (otomatik çekiliyor)
- Manuel Kayıtlar: Yöneticiler tarafından manuel oluşturulan iade talepleri
- Manuel İade Kaydı Oluşturma:
- Sipariş arama (numara, müşteri ismi, e-posta)
- 7 farklı iade nedeni (Hasarlı, Yanlış, Kusurlu, vb.)
- Kargo takip numarası girişi
- Otomatik timeline event oluşturma
- İade Detay Sayfası:
- Sipariş bilgileri ve müşteri detayları
- Durum güncelleme (Beklemede, İşlemde, Tamamlandı, Reddedildi)
- Not ekleme sistemi
- Zaman çizelgesi (timeline) görüntüleme
- Ayarlar:
- Portal aktif/pasif yapma
- Otomatik subdomain oluşturma (örn: paen.enestekin.com)
- Özel domain ayarlama (örn: iade.magaza.com)
- Entegrasyon örnekleri (e-posta, web sitesi, WhatsApp/SMS)
4 adımlı kolay iade süreci:
- Sipariş Doğrulama: Sipariş numarası + e-posta ile doğrulama
- İade Nedeni Seçimi: 7 farklı iade nedeni kartları
- Fotoğraf Yükleme: Hasarlı/yanlış/kusurlu ürünler için (max 5 fotoğraf)
- İade Talimatları: Paket nasıl gönderilir + talep gönderimi
- Next.js 15 + App Router with React 19 and TypeScript
- Multi-tenant Subdomain System: Wildcard domain support with automatic subdomain generation
- Next.js Middleware: Edge runtime for subdomain routing and tenant identification
- OAuth for ikas: end-to-end flow (authorize → callback → session/JWT)
- Admin GraphQL client:
@ikas/admin-api-clientwith codegen - Prisma ORM: SQLite (dev) / PostgreSQL (production ready)
- Tailwind CSS v4: Modern ve responsive UI
- Iron Session: Server-side session management
- Timeline System: Event-based activity tracking
- Multi-step Forms: SessionStorage ile state management
- Public API Endpoints: JWT gerektirmeyen müşteri endpoint'leri
- Rate Limiting: IP-based in-memory rate limiting for security
- Performance Optimizations: Proactive token refresh, lightweight queries, retry mechanism
src/
├─ app/
│ ├─ api/
│ │ ├─ ikas/
│ │ │ ├─ get-merchant/route.ts # Example secure API route (JWT required)
│ │ │ ├─ orders/route.ts # Sipariş arama endpoint
│ │ │ └─ refund-orders/route.ts # İade durumundaki siparişler (90 gün)
│ │ ├─ public/ # JWT gerektirmeyen public endpoints
│ │ │ ├─ verify-order/route.ts # Müşteri sipariş doğrulama
│ │ │ └─ submit-refund/route.ts # Müşteri iade gönderimi
│ │ ├─ refunds/
│ │ │ ├─ route.ts # İade listesi ve oluşturma
│ │ │ └─ [id]/
│ │ │ ├─ route.ts # İade detay ve güncelleme
│ │ │ └─ timeline/route.ts # İade timeline eventleri
│ │ ├─ settings/route.ts # Mağaza ayarları
│ │ └─ oauth/
│ │ ├─ authorize/ikas/route.ts # OAuth başlatma
│ │ └─ callback/ikas/route.ts # OAuth callback
│ │
│ ├─ dashboard/page.tsx # Ana dashboard (3 navigasyon kartı)
│ ├─ refunds/
│ │ ├─ page.tsx # İki sekme: ikas + manuel kayıtlar
│ │ ├─ new/page.tsx # Manuel iade kaydı oluşturma
│ │ └─ [id]/page.tsx # İade detay sayfası
│ ├─ settings/page.tsx # Ayarlar sayfası (portal URL vb.)
│ ├─ portal/ # Müşteri self-service portalı
│ │ ├─ page.tsx # Adım 1: Sipariş doğrulama
│ │ ├─ reason/page.tsx # Adım 2: İade nedeni seçimi
│ │ ├─ upload/page.tsx # Adım 3: Fotoğraf yükleme
│ │ └─ complete/page.tsx # Adım 4: Tamamlama
│ ├─ authorize-store/page.tsx # Manual store authorization
│ ├─ callback/page.tsx # OAuth callback handler
│ ├─ page.tsx # Entry point
│ └─ hooks/use-base-home-page.ts # Auth/bootstrap logic
│
├─ components/
│ ├─ home-page/index.tsx # Simple authenticated UI
│ └─ ui/* # shadcn/ui components
│
├─ helpers/
│ ├─ api-helpers.ts # getIkas(), onCheckToken()
│ ├─ jwt-helpers.ts # JWT create/verify
│ ├─ token-helpers.ts # Token utilities
│ └─ subdomain-helpers.ts # Subdomain generation, validation, lookup
│
├─ lib/
│ ├─ api-requests.ts # Frontend → backend bridge
│ ├─ auth-helpers.ts # getUserFromRequest()
│ ├─ ikas-client/
│ │ ├─ graphql-requests.ts # GraphQL queries/mutations
│ │ └─ generated/graphql.ts # Generated types
│ ├─ prisma.ts # Prisma client
│ ├─ rate-limit.ts # Rate limiting utilities
│ └─ session.ts # iron-session wrappers
│
├─ middleware.ts # Next.js Edge middleware (subdomain routing)
│
└─ models/
└─ auth-token/ # Token management
- AuthToken # OAuth token'ları
- RefundRequest # İade talepleri (orderId, status, reason, trackingNumber)
- RefundNote # İade notları
- RefundTimeline # İade event history
- Merchant # Mağaza ayarları (subdomain, portalUrl, portalEnabled)
- RestrictedSubdomain # Yasaklı subdomain listesi (www, api, admin, vb.)
Live URLs:
- Main App: https://refund-v1.vercel.app
- Example Portal: https://paen.enestekin.com (subdomain-based multi-tenant portal)
Deployment Platform: Vercel (Serverless) Database: Neon PostgreSQL Domain: enestekin.com with wildcard support (*.enestekin.com)
Deployment Status: ✅ Aktif ve çalışıyor
📚 Deployment Rehberi: Production ortamına deploy etmek için DEPLOYMENT_GUIDE.md dosyasına bakın.
- Install dependencies
pnpm install- Create env file and set variables
cp .env.example .env.localRequired envs (see src/globals/config.ts):
NEXT_PUBLIC_GRAPH_API_URL— ikas Admin GraphQL URL (e.g.https://api.myikas.com/api/v2/admin/graphql)NEXT_PUBLIC_ADMIN_URL— ikas Admin base with{storeName}placeholder (e.g.https://{storeName}.myikas.com/admin)NEXT_PUBLIC_CLIENT_ID— your ikas app client idCLIENT_SECRET— your ikas app client secretNEXT_PUBLIC_DEPLOY_URL— public base URL of this app (e.g.https://yourapp.example.com)SECRET_COOKIE_PASSWORD— long random string for iron-session
- Initialize Prisma (first run)
pnpm prisma:init- Generate GraphQL types (whenever you change
graphql-requests.ts)
pnpm codegen- Start dev server
pnpm devPort and redirect path are also defined in ikas.config.json:
{
"portMapping": { "default": 3000 },
"oauthRedirectPath": "/api/oauth/callback/ikas",
"runCommand": "pnpm run dev"
}pnpm dev— start Next.js in devpnpm build— build productionpnpm start— start production serverpnpm lint— run ESLintpnpm codegen— GraphQL Codegen usingsrc/lib/ikas-client/codegen.tspnpm prisma:init— generate client and push schema to local DBpnpm prisma:migrate— create/apply migrationspnpm prisma:generate— regenerate Prisma clientpnpm prisma:studio— open Prisma Studiopnpm apply:ai-rules— apply Ruler agent configs
The application uses a subdomain-based multi-tenant architecture where each merchant gets their own subdomain:
-
Automatic Generation: When a merchant completes OAuth onboarding, a subdomain is automatically generated from their store name
- Example: "Paen Mağazası" →
paen.enestekin.com - Turkish character support: "Çiçek Dünyası" →
cicek-dunyasi.enestekin.com - Conflict resolution: If subdomain exists, appends number (
paen-2.enestekin.com)
- Example: "Paen Mağazası" →
-
Middleware Routing: Next.js Edge middleware detects subdomains and routes requests
- System subdomains pass through:
www,api,admin,app,dashboard - Tenant subdomains redirect root (
/) to/portal - Adds
x-subdomainheader for downstream processing
- System subdomains pass through:
-
Wildcard DNS: Vercel configured with
*.enestekin.comto handle all subdomains -
Database Schema:
model Merchant { subdomain String? @unique subdomainStatus String @default("pending") // pending, active subdomainChangedAt DateTime? subdomainChangeCount Int @default(0) }
// Generate URL-safe subdomain from store name
const subdomain = await SubdomainHelpers.generateSubdomain("Paen Mağazası");
// Returns: "paen"
// Check if subdomain is available
const available = await SubdomainHelpers.isSubdomainAvailable("paen");
// Returns: true/false
// Get merchant ID from subdomain
const merchantId = await SubdomainHelpers.getMerchantBySubdomain("paen");
// Returns: merchantId or null
// Build full portal URL
const url = SubdomainHelpers.buildPortalUrl("paen");
// Returns: "https://paen.enestekin.com"-
Middleware Layer (Edge Runtime):
- Detects subdomain from
Hostheader - Validates against system subdomains
- Adds headers for tenant identification
- No database calls (Edge runtime limitation)
- Detects subdomain from
-
API Layer (Serverless Functions):
- Reads
x-subdomainheader from middleware - Performs database lookup for merchant
- Validates subdomain status (
activeonly) - Rate limiting per IP address
- Reads
-
Database Layer:
- Unique subdomain constraint
- Subdomain status tracking
- Restricted subdomain table for blocked names
-
User starts at
/which runsuse-base-home-page:- If embedded (iFrame) and a valid token exists via
TokenHelpers.getTokenForIframeApp(), redirect to/dashboard. - Otherwise, if
storeNameis present in query, redirect to/api/oauth/authorize/ikas?storeName=.... - Else route to
/authorize-storewhere user enters store name.
- If embedded (iFrame) and a valid token exists via
-
GET /api/oauth/authorize/ikasvalidatesstoreName, setsstatein session, and redirects to ikas authorize URL. -
GET /api/oauth/callback/ikasvalidates thesignatureparameter using HMAC-SHA256 (viaTokenHelpers.validateCodeSignature), optionally validatesstatefor CSRF protection, exchangescodefor tokens, fetchesgetMerchantandgetAuthorizedApp, upserts token viaAuthTokenManager, sets session, builds a short-lived JWT viaJwtHelpers.createToken, and redirects to/callback?.... -
/callback(client) readstoken,redirectUrl,authorizedAppId, stores token insessionStorage, then redirects back to Admin.
The OAuth callback endpoint requires a signature query parameter to validate the authorization code:
- Signature Generation:
HMAC-SHA256(code, clientSecret)in hex format - Validation:
TokenHelpers.validateCodeSignature(code, signature, clientSecret) - State Parameter: Optional but recommended for additional CSRF protection
- Browser obtains JWT via AppBridge or OAuth callback and stores it in
sessionStorage. - Frontend calls backend routes with
Authorization: JWT <token>. - Example backend route:
GET /api/ikas/get-merchantusesgetUserFromRequest()to extractmerchantIdandauthorizedAppId, loads the stored token viaAuthTokenManager, creates GraphQL client withgetIkas(), then callsikasClient.queries.getMerchant().
Frontend bridge (src/lib/api-requests.ts):
ApiRequests.ikas.getMerchant(token) // -> GET /api/ikas/get-merchant- Define documents in
src/lib/ikas-client/graphql-requests.tsusinggql:
export const GET_MERCHANT = gql`
query getMerchant { getMerchant { id email storeName } }
`;- Run
pnpm codegento regeneratesrc/lib/ikas-client/generated/graphql.ts. - Create client via
getIkas(token)which auto-refreshes tokens inonCheckToken. - Use:
ikasClient.queries.getMerchant()orikasClient.mutations.someMutation(vars).
MCP guidance (required before adding new ops):
- Discover operation with ikas MCP list, then introspect shape.
- Add to
graphql-requests.ts, then runpnpm codegen.
The system has been optimized for performance with the following improvements:
- Proactive Token Refresh: Tokens are refreshed 5 minutes BEFORE expiry (not after)
- Cached Token Checks: Reduces API calls during high-traffic periods
- Token expiry calculation:
const fiveMinutesBeforeExpiry = expireDate.getTime() - (5 * 60 * 1000); if (now.getTime() >= fiveMinutesBeforeExpiry) { // Refresh token proactively }
- Lightweight Queries: Portal uses minimal field queries (7 fields instead of 20+)
- Pagination: All list queries use pagination limits
- Example optimized query:
query verifyOrder($orderNumber: StringFilterInput, $pagination: PaginationInput) { listOrder(orderNumber: $orderNumber, pagination: $pagination) { data { id orderNumber totalFinalPrice currencySymbol orderedAt customer { email firstName lastName } } } }
- Simplified Merchant Lookup: Single query instead of multiple joins
- Indexed Fields: Unique indexes on
subdomain,orderIdfor fast lookups - Connection Pooling: Prisma connection pooling for Neon PostgreSQL
- Retry Mechanism: Automatic retry with 2-second delay for transient failures
- Timeout Configuration: Vercel function timeout increased to 30 seconds
- Graceful Degradation: User-friendly error messages with retry suggestions
- Portal response time: 30+ seconds → 3-5 seconds (10x improvement)
- Token refresh overhead: Eliminated during peak requests
- Database query time: Reduced by 60% with optimized queries
- Local SQLite DB located under
prisma/dev.dbwith schema managed byschema.prisma. AuthTokenManagerpersists tokens (models/auth-token/*).- Use Prisma Studio to inspect tokens:
pnpm prisma:studio- Tailwind v4 with CSS file at
src/app/globals.css. - shadcn/ui components under
src/components/ui/*.
- UI scaffolding: use shadcn MCP to fetch components/demos and place under
src/components/ui/*. - ikas GraphQL: use ikas MCP list + introspect before adding operations.
- Never log secrets or tokens. Do not expose access/refresh tokens to the client.
- Use the short-lived JWT for browser → server auth; server uses stored OAuth tokens.
onCheckTokenauto-refreshes tokens server-side (5 minutes before expiry).- OAuth callback uses HMAC-SHA256 signature validation to verify authorization code authenticity before token exchange.
- Rate Limiting: IP-based rate limiting on public endpoints to prevent abuse:
- Order verification: 10 requests/minute per IP
- Refund tracking: 20 requests/minute per IP
- Rate limit headers included in responses (X-RateLimit-*)
- Security logging for rate limit violations
- Multi-tenant isolation: Subdomain-based tenant identification with database-level isolation
MIT
- Use Conventional Commits. Example:
feat(auth): add token refresh on client - Ensure type-safety and linter cleanliness.
- ikas Admin GraphQL:
https://api.myikas.com/api/v2/admin/graphql - File issues or questions in this repo.