Curva ✕ Pasukuru → LINE-native ecommerce (review 2026-05-07)
User vision: single product = LINE-native ecommerce. Everything done inside LINE. Pasukuru = ecommerce backbone + Mini Apps. Curva = LINE OA control plane + chat + scenarios.
This is a review only — no code changes.
TL;DR
Highly feasible. Integration scaffold already ~30% built in both repos (PartnerConnection model in Curva, app/integration/ module in Pasukuru BE, BullMQ webhook queue, payload builders, key exchange flow, settings UI in Curva). Remaining = LIFF wrapping + identity mapping + LINE Pay + 2-way webhook receivers + order events.
1. Repo snapshots
Curva (COCONRobotics-Corp/Curva)
- Stack: Laravel 13 + Inertia/Vue + Jetstream + Sanctum + Reverb + Cashier + Horizon
- LINE SDK:
linecorp/line-bot-sdk: ^11.2(Messaging API) - LINE-native primitives already shipped (models):
LineAccount(one per tenant LINE OA),LineFollower,LineFollowerCustomField/Value,LineFollowerScenario,LineFollowerTimelineLineRichMenu+Action+ActionDetail+ActionItem+AreaPreset+AreaPresetDetail+Image+FolderLineLiff(LIFF app registry per LineAccount)LineFlexMessage+Content+ContentVersionLineQrCode+QrTrackingSession(deep-link campaign tracking)BulkMessage+BulkMessageContentScenario+ScenarioContent+ScenarioContentBody+ScenarioDeliveryHistoryLineWebhookMessage,LineTaxonomy
- Webhook handler:
Api/Line/LineWebhookController— signature validation +LineEventHandlerFactory - LIFF apps live:
SurveyLiffController,QrTrackingLiffController(/liff/*routes) - Other: Zoom, Notion, Stripe Cashier (Curva tenant subs), Reverb (websockets), Horizon
- Routes:
web-admin.php,web-developer.php,web-member.php,web.php,api.php
Pasukuru BE (COCONRobotics-Corp/pasukuru-be)
- Stack: NestJS 10 + TypeORM + MySQL + Redis + BullMQ + Stripe@19 + Puppeteer + Mailer + AWS SES/Route53
- Modules:
auth(4 JWT strategies: admin / master-admin / member / user),master(category, item, attribute, layout, article, page, agent, agent-profit, sale-type, payment-method, order-status, item-label, member, prefecture-type, file, dashboard, mail-domains, shipping-method, user-mail, user-group, stripe-payment-status, setting, domain),order,integration - Multi-tenancy:
DomainAdminMiddleware— resolves tenant fromHostheader →req.adminId+req.domain - Order:
Order+OrderPayment+OrderStripeEvententities - NO LINE deps — clean commerce backbone
Pasukuru FE (COCONRobotics-Corp/pasukuru-fe)
- Stack: Next.js 16 (default) + Tailwind 4 + React Query + dnd-kit + milkdown + tiptap + monaco + radix-ui + antd + react-hook-form + zod
- Routes:
cart/,checkout/,news/,(landing-page)/,(auth)/,(page-builder)/,(confirmation-payment)/,dashboard/,_public-renderer/,api/ - Page builder: Coco Robo (dnd-kit blocks)
- Services: 25+ (products, pages, orders, members, agents, agent-rewards, integration, payment-method, layout, articles, …)
- NO LINE/LIFF SDK currently
2. Integration scaffold ALREADY shipped (both sides)
Curva → Pasukuru
| Piece | Path |
|---|---|
| Model | app/Models/PartnerConnection.php (PARTNER_KURU = 'kuru', line_account_id, partner_api_key, partner_shop_id, active) |
| Model | app/Models/IntegrationKey.php |
| Service | app/Services/Integration/PartnerConnectionService.php (connect / disconnect / test / register / registerCurvaWithPasukuru) |
| Service | app/Services/Integration/IntegrationKeyService.php (per-LineAccount key get/generate) |
| Controller | app/Http/Controllers/Api/IntegrationController.php (myKey, connectKuru, testKuru, disconnectKuru, ping, registerKuru, unregisterKuru) |
| Routes (api.php) | GET integration/ping, POST integration/register-kuru, POST integration/unregister-kuru (gated by validate.curva.api.key middleware) |
| Settings UI | resources/js/Pages/Member/LineAccounts/Settings/Index.vue — has form fields: Pasukuru API key + kuru_shop_id |
Pasukuru BE → Curva
| Piece | Path |
|---|---|
| Module | src/app/integration/ |
| Controller | integration.controller.ts — api/v1/integration/{my-key, connect/curva, connect/curva/test, ping, register-curva-webhook} |
| Service | integration.service.ts (getOrCreateKey, saveConnection, getConnection, testConnection, disconnect, registerCurvaWebhook) |
| Guard | KuruApiKeyGuard (inbound from Curva) + AdminJwtAuthGuard (admin UI) |
| Queue | WebhookQueueService → BullMQ queue curva-webhook, 5 attempts, exponential backoff (5s base), retention 24h ok / 30d fail |
| Emitter | WebhookEventEmitter — fires product.created/updated/deleted, shop.updated |
| Builders | product-payload.builder.ts, shop-payload.builder.ts |
| Listeners | webhook.listeners.ts |
| Types | WebhookPayload<T>{event, entity, shop_id, data}, ProductData{id,name,description,price,image_url,category}, ShopData{shop_profile, policies} |
| DTOs | ConnectCurvaDto, RegisterCurvaWebhookDto{curvaApiKey, curvaLineAccount} |
Handshake flow (already implemented)
- Pasukuru admin opens settings → calls
GET /api/v1/integration/my-key→ receivespasukuruApiKey - Admin gives key +
shopIdto Curva tenant - In Curva LineAccount Settings → submits
kuru_api_key+kuru_shop_id - Curva
PartnerConnectionService::connect()→testKuruKey($kuruApiKey)(calls Pasukuru/integration/pingwith bearer)keyService->getOrGenerate($lineAccount->id, 'kuru')→ returnscurvaApiKeyregisterCurvaWithPasukuru($kuruApiKey, $basicId, $curvaApiKey)— POSTs to Pasukuru/api/v1/integration/register-curva-webhookwith{curvaApiKey, curvaLineAccount: basicId}- Saves
PartnerConnectionrow
- Now: Pasukuru holds
curvaApiKeyfor outbound webhooks; Curva holdskuruApiKeyfor ping/register/unregister inbound.
Solid B2B-style key exchange. Ship-ready foundation.
3. What’s missing for full LINE-native ecommerce
A. LIFF wrapper for Pasukuru FE (BLOCKER)
- Pasukuru FE has zero LIFF SDK / LINE login.
- For “shopfront-inside-LINE”, Pasukuru FE must:
- Add
@line/liffSDK to package.json - On boot inside LIFF context:
liff.init({ liffId })→liff.getProfile()→line_user_id - Send
line_user_id(verifiedidToken) to BE → BE looks up or createsMemberlinked to that line_user_id (per tenant/domain)
- Add
- Curva’s
LineLiffmodel already stores LIFF apps per LineAccount → 1 row per Pasukuru tenant front-end (e.g. shop, cart, account, history)
B. Identity mapping (BLOCKER)
- Currently:
- Curva:
LineFollower.line_user_id(perLineAccount) - Pasukuru:
Member/User(peradmin_id/domain), no LINE field
- Curva:
- Need: column
line_user_id(nullable, indexed, unique-per-tenant) on Pasukurumemberentity - Bridge endpoint Pasukuru-side:
POST /api/v1/integration/line/follower-link→ upserts member by line_user_id - Allows: same human = Curva LineFollower = Pasukuru Member, no double-account
C. Outbound order events (Pasukuru → Curva)
- Current emitter only handles
product.*+shop.updated. Add:order.created/order.paid/order.shipped/order.cancelled- Payload: orderId, lineUserId (resolved at emit time), itemSummary, total, status
- Curva side: needs inbound webhook receiver for these events (currently NOT in repo). New route:
POST api/v1/integration/pasukuru/webhookwith HMAC verification → dispatches LINE Flex Message via existingLineFlexMessageServiceto follower with matching line_user_id.
D. Curva → Pasukuru product feed for Flex Messages
- Curva already has FlexMessage builder UI (
LineFlexMessageController) - Add: “Insert Pasukuru product” button → fetches
GET /api/v1/integration/pasukuru/items(new endpoint) → renders Flex Message carousel from product cards - Pasukuru: just another guarded read endpoint using
KuruApiKeyGuard
E. Rich menu → LIFF deep links
LineRichMenuActionalready supports URL action types- Build helper in Curva UI: dropdown “Open Pasukuru shop / cart / account / orders” → auto-fills LIFF URL
https://liff.line.me/{liffId}from registeredLineLiffrows for that LineAccount - Zero schema change — only UX
F. LINE Pay (NICE-TO-HAVE)
- Pasukuru BE has Stripe + PayPay (per memory). LINE Pay would close LINE-native loop (less context-switch).
- Effort: new
payment-methodrow + LINE Pay SDK (or REST) + webhook handler — pattern matches existing Stripe wiring - Not strictly required (Stripe redirect from LIFF works, LIFF supports
liff.openWindowfor Stripe Checkout)
G. Agent/MLM × LINE QR (HIGH ROI, low effort)
- Pasukuru has
agent+agent-profit(referral commissions) - Curva has
LineQrCode+QrTrackingSession(campaign QR tracking) - Bridge: agent-specific QR → Curva tracks → on add-friend → scenario assigns
partner_shop_id + agent_code→ Pasukuru webhook → member created with referrer set - ZERO new infra, just glue inside
Scenarioconfig
4. Architecture target
LINE App
│
├─ Tap LINE OA (Curva managed) ───────────────┐
│ │
├─ Receive Flex Msg with Pasukuru products ◄──┤ Curva (Laravel)
│ (built from product feed cache) │ - Messaging API
│ │ - Webhook recv (in/out)
├─ Tap card → opens LIFF │ - Scenario / Bulk / RichMenu
│ │ │ - LineFollower DB
│ ▼ │
│ LIFF (Pasukuru FE wrapped) ────────────► │
│ │ liff.getProfile() │
│ │ → line_user_id │
│ │ │
│ ▼ │
│ Pasukuru BE (NestJS, multi-tenant) │
│ - Member upsert by line_user_id │
│ - Cart / Order / Stripe / PayPay │
│ - Webhook OUT ─────────────────────────► │ Curva /pasukuru/webhook
│ product.* / shop.* / order.* │ → Flex Msg push to LINE
│ │
└─ Get receipt / shipping push in LINE chat ◄──┘
5. Possibility matrix
| Capability | Possible? | Effort | Existing scaffold |
|---|---|---|---|
| LINE login + LIFF in Pasukuru shopfront | ✅ | M | LineLiff model exists in Curva; add @line/liff to FE |
| Cross-system identity (line_user_id ↔ member) | ✅ | S | Add column + bridge endpoint |
| Product feed Curva ← Pasukuru | ✅ | S | Builder + KuruApiKeyGuard exist |
| Flex Message product cards | ✅ | S | FlexMessage builder UI exists |
| Rich menu → Pasukuru LIFF | ✅ | XS | URL action type + LineLiff registry |
| Pasukuru → Curva webhooks (product/shop) | ✅ partial | M | Outbound emitter+queue done; Curva inbound receiver missing |
| Order event webhooks | ✅ | M | Emitter pattern exists; need order.* events + Curva receiver |
| Stripe Checkout from LIFF | ✅ | XS | liff.openWindow works |
| LINE Pay | ✅ | M | New payment-method + SDK |
| Agent referral via LINE QR | ✅ | S | LineQrCode + agent-profit both exist; glue only |
| In-LINE customer support chat | ✅ | XS | Curva Member/ChatController + ConversationAssignment exist |
| Push reminders / abandoned cart | ✅ | S | Scenario engine exists; trigger on Pasukuru events |
| Multi-shop per LINE OA | ⚠️ | L | Today: 1 LineAccount ↔ 1 PartnerConnection. Schema 1:1. Multi needs rework |
| Bot-driven product search | ✅ | M | Webhook handler factory + product feed |
Not possible without major rework:
- Native LINE in-app payment without redirect (LINE Pay closest, still has consent screen)
- Multi-tenant Pasukuru sharing one LINE OA (current key exchange is 1:1)
6. Recommended sequencing (review only — no code yet)
Phase 0 — finish what’s started (1 sprint)
- Curva: add inbound webhook receiver
POST /api/integration/pasukuru/webhook(HMAC verify) → routes events toLineFlexMessageServicefor product cards / receipts. - Pasukuru BE: emit
order.created/order.paid/order.shipped/order.cancelledevents into existing BullMQ pipeline. - Pasukuru BE: add
member.line_user_id(nullable, indexed, unique within admin scope).
Phase 1 — LIFF wrap (1-2 sprints)
4. Pasukuru FE: install @line/liff, init in _public-renderer and cart/checkout flows.
5. New endpoint POST /api/v1/integration/line/identify — accepts liff idToken → verifies via LINE → upserts Member.
6. Curva: register Pasukuru LIFF apps in LineLiff table per LineAccount (admin tool).
Phase 2 — UX glue (1 sprint) 7. Curva FlexMessage UI: “Insert Pasukuru product” picker. 8. Curva RichMenu UI: “Open Pasukuru shop/cart/account” preset buttons. 9. Curva Scenario: “abandoned cart” / “post-purchase upsell” pre-built nodes.
Phase 3 — Payment + agent (1 sprint) 10. LINE Pay payment-method on Pasukuru. 11. Agent QR → Curva campaign QR → auto-link on follow.
Phase 4 — multi-shop (only if needed)
12. Promote PartnerConnection to many-to-many; tag flex messages with shop_id selection per scenario step.
7. Risk / friction points
- JP cookie / iframe — LIFF runs in WebView; Pasukuru auth cookies must use
SameSite=None; Secure. - Multi-tenant resolution inside LIFF — Pasukuru today resolves tenant from
Host. Inside LIFF, must use either (a) per-tenant LIFF subdomain or (b) tenant-aware path routing or (c) JWT-encodedshop_idfrom Curva on LIFF init. - HMAC + replay — outbound webhooks need signed timestamp + nonce (current
KuruApiKeyGuardis bearer only). - GDPR/PI — sharing line_user_id between systems = personal data, need consent in LIFF on first use.
- LINE rate limits — push messages capped by LINE plan; high-volume order receipts may need batching via Curva’s existing BulkMessage pipeline.
- NO-TOUCH boundary: this work is CRV (Curva) + PASS (Pasukuru) Jira projects only. Da Vinci untouched. ✅
8. Verdict
Go. The architecture, key-exchange, queue, and tenant boundaries are already in place. ~70% remaining is glue + LIFF wrapping + 2-way webhook completion. No blockers, no architectural rewrites needed.
The pre-existing partner='kuru' references in both repos are unambiguous — this integration was clearly planned from day one. We’re completing it, not inventing it.