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, LineFollowerTimeline
    • LineRichMenu + Action + ActionDetail + ActionItem + AreaPreset + AreaPresetDetail + Image + Folder
    • LineLiff (LIFF app registry per LineAccount)
    • LineFlexMessage + Content + ContentVersion
    • LineQrCode + QrTrackingSession (deep-link campaign tracking)
    • BulkMessage + BulkMessageContent
    • Scenario + ScenarioContent + ScenarioContentBody + ScenarioDeliveryHistory
    • LineWebhookMessage, 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 from Host header → req.adminId + req.domain
  • Order: Order + OrderPayment + OrderStripeEvent entities
  • 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

PiecePath
Modelapp/Models/PartnerConnection.php (PARTNER_KURU = 'kuru', line_account_id, partner_api_key, partner_shop_id, active)
Modelapp/Models/IntegrationKey.php
Serviceapp/Services/Integration/PartnerConnectionService.php (connect / disconnect / test / register / registerCurvaWithPasukuru)
Serviceapp/Services/Integration/IntegrationKeyService.php (per-LineAccount key get/generate)
Controllerapp/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 UIresources/js/Pages/Member/LineAccounts/Settings/Index.vue — has form fields: Pasukuru API key + kuru_shop_id

Pasukuru BE → Curva

PiecePath
Modulesrc/app/integration/
Controllerintegration.controller.tsapi/v1/integration/{my-key, connect/curva, connect/curva/test, ping, register-curva-webhook}
Serviceintegration.service.ts (getOrCreateKey, saveConnection, getConnection, testConnection, disconnect, registerCurvaWebhook)
GuardKuruApiKeyGuard (inbound from Curva) + AdminJwtAuthGuard (admin UI)
QueueWebhookQueueService → BullMQ queue curva-webhook, 5 attempts, exponential backoff (5s base), retention 24h ok / 30d fail
EmitterWebhookEventEmitter — fires product.created/updated/deleted, shop.updated
Buildersproduct-payload.builder.ts, shop-payload.builder.ts
Listenerswebhook.listeners.ts
TypesWebhookPayload<T>{event, entity, shop_id, data}, ProductData{id,name,description,price,image_url,category}, ShopData{shop_profile, policies}
DTOsConnectCurvaDto, RegisterCurvaWebhookDto{curvaApiKey, curvaLineAccount}

Handshake flow (already implemented)

  1. Pasukuru admin opens settings → calls GET /api/v1/integration/my-key → receives pasukuruApiKey
  2. Admin gives key + shopId to Curva tenant
  3. In Curva LineAccount Settings → submits kuru_api_key + kuru_shop_id
  4. Curva PartnerConnectionService::connect()
    • testKuruKey($kuruApiKey) (calls Pasukuru /integration/ping with bearer)
    • keyService->getOrGenerate($lineAccount->id, 'kuru') → returns curvaApiKey
    • registerCurvaWithPasukuru($kuruApiKey, $basicId, $curvaApiKey) — POSTs to Pasukuru /api/v1/integration/register-curva-webhook with {curvaApiKey, curvaLineAccount: basicId}
    • Saves PartnerConnection row
  5. Now: Pasukuru holds curvaApiKey for outbound webhooks; Curva holds kuruApiKey for 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/liff SDK to package.json
    • On boot inside LIFF context: liff.init({ liffId })liff.getProfile()line_user_id
    • Send line_user_id (verified idToken) to BE → BE looks up or creates Member linked to that line_user_id (per tenant/domain)
  • Curva’s LineLiff model 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 (per LineAccount)
    • Pasukuru: Member / User (per admin_id / domain), no LINE field
  • Need: column line_user_id (nullable, indexed, unique-per-tenant) on Pasukuru member entity
  • 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/webhook with HMAC verification → dispatches LINE Flex Message via existing LineFlexMessageService to 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
  • LineRichMenuAction already 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 registered LineLiff rows 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-method row + LINE Pay SDK (or REST) + webhook handler — pattern matches existing Stripe wiring
  • Not strictly required (Stripe redirect from LIFF works, LIFF supports liff.openWindow for 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 Scenario config

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

CapabilityPossible?EffortExisting scaffold
LINE login + LIFF in Pasukuru shopfrontMLineLiff model exists in Curva; add @line/liff to FE
Cross-system identity (line_user_id ↔ member)SAdd column + bridge endpoint
Product feed Curva ← PasukuruSBuilder + KuruApiKeyGuard exist
Flex Message product cardsSFlexMessage builder UI exists
Rich menu → Pasukuru LIFFXSURL action type + LineLiff registry
Pasukuru → Curva webhooks (product/shop)✅ partialMOutbound emitter+queue done; Curva inbound receiver missing
Order event webhooksMEmitter pattern exists; need order.* events + Curva receiver
Stripe Checkout from LIFFXSliff.openWindow works
LINE PayMNew payment-method + SDK
Agent referral via LINE QRSLineQrCode + agent-profit both exist; glue only
In-LINE customer support chatXSCurva Member/ChatController + ConversationAssignment exist
Push reminders / abandoned cartSScenario engine exists; trigger on Pasukuru events
Multi-shop per LINE OA⚠️LToday: 1 LineAccount ↔ 1 PartnerConnection. Schema 1:1. Multi needs rework
Bot-driven product searchMWebhook 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)

Phase 0 — finish what’s started (1 sprint)

  1. Curva: add inbound webhook receiver POST /api/integration/pasukuru/webhook (HMAC verify) → routes events to LineFlexMessageService for product cards / receipts.
  2. Pasukuru BE: emit order.created / order.paid / order.shipped / order.cancelled events into existing BullMQ pipeline.
  3. 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-encoded shop_id from Curva on LIFF init.
  • HMAC + replay — outbound webhooks need signed timestamp + nonce (current KuruApiKeyGuard is 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.