
L Harness
Self-hosted LINE contact CRM, chat and messaging workflows
L Harness is a self-hosted L Message alternative built on Cloudflare (D1, Durable Objects, Pages, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.
Source & license
Upstream license: MIT
License TL;DR
You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.
Explain MIT in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
A small self-hosted CRM can fit the documented Cloudflare free allowances. A LINE Official Account and Messaging API channel are mandatory: The Japan Communication Plan includes up to 200 charge-counted messages/month at no monthly fee; higher-volume delivery incurs LINE plan fees. Optional Claude/AI services and custom domains are separate costs.
Hosting requirements
- Operate within the region-specific LINE messaging plan and current delivery allowance; Cloudflare free hosting does not make unlimited LINE sends free.
- Keep Worker/D1/R2/SQLite-DO use within free quotas and configure your own channel credentials, owner account and LIFF application. External AI is optional.
- Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
Sources checked 01/10/2026
Repository snapshot: 5386460. Hosting eligibility reflects the deployment documentation and listed assumptions.
- l-message ↗
LINE 公式アカウントの完全オープンソース CRM。**L社 / U社 の無料代替**。 L Harness本体のソフトウェア利用料は **0円** です。LINE公式アカウントの配信料金、Cloudflareの契約・利用量、独自ドメイン、導入・保守、接続する外部AIサービスの費用は別です。Claude CodeからMCP経由で操作できます。
- pages ↗
name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"
- workers ↗
name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enab
- d1 ↗
ASSETS.fetch(req) を呼んで # SPA を返す。これが無いと static asset path が Worker をバイパスして # 直接配信され、bot 判定が効かない。 run_worker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib
- r2 ↗
orker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきに
- durable-objects ↗
images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきにくい壊れ方をするので要注意。 # 実装: apps/worker/src/durable-objects/tenant-scheduler.ts # ═══════════════════════════════════════════════════════════════ [[durable_objects.bindings]] name = "TENANT_SCHEDULER" cl
- free-tier-eligible ↗
name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"
- free-tier-eligible ↗
| **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |
- free-tier-eligible ↗
| Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |
- free-tier-eligible ↗
| Storage | 10 GB-month / month |
- free-tier-eligible ↗
Durable Objects are available both on Workers Free and Workers Paid plans.
- free-tier-eligible ↗
--><tr><!--[--><th align="left"><!--[--><!--]--></th><th align="center"><!--[-->Communication Plan<!--]--></th><th align="center"><!--[-->Light Plan<!--]--></th><th align="center"><!--[-->Standard Plan<!--]--></th><!--]--></tr><!--]--></thead><tbody><!--[--><tr><!--[--><td align="left"><!--[-->Fixed monthly fee <sup><a href="#user-content-fn-1" class="" aria-describedby="footnote-label" data-footnote-ref id="user-content-fnref-1"><!--[--><!--[-->1<!--]--><!--]--></a></sup><!--]--></td><td align="center"><!--[-->Free<!--]--></td><td align="center"><!--[-->JPY 5,000<!--]--></td><td align="center"><!--[-->JPY 15,000<!--]--></td><!--]--></tr><tr><!--[--><td align="left"><!--[-->Number of free messages per month<!--]--></td><td align="center"><!--[-->Up to 200<!--]--></td><td align="center"><!--[-->Up to 5,000<!--]--></td><td align="center"><!--[-->Up to 30,000<!--]--></td><!--]--></tr><tr><!--[--><td align="left"><!--[-->Cost of additional messages <sup><a href="#user-content-fn-1" class="" aria-describedby="footnote-label" data-footnote-ref id="user-content-fnref-1-2"><!--[--><!--[-->1<!--]--><!--]--></a></sup><!--]--></td><td align="center"><!--[-->N/A<!--]--></td><td align="center"><!--[-->N/A<!--]--></td><td align="center"><!--[-->Up to JPY 3/message <sup><a href="#user-content-fn-2" class="" aria-describedby="footnote-label" data-footnote-ref id="user-conte
- free-tier-eligible ↗
| Class A Operations | 1 million requests / month |
- free-tier-eligible ↗
| Class B Operations | 10 million requests / month |
- free-tier-eligible ↗
| Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows |
- free-tier-eligible ↗
| SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month |
- MIT ↗
MIT License Copyright (c) 2026 Shudesu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- architecture ↗
name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"
- architecture ↗
name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enabled only for the private test Worker; production is # explicitly disabled below until cache hit rate and auth isolation are proven. # Only routes with an explicit public Cache-Control header are eligible because # src/middleware/cache-policy.ts marks every other response no-store. [cache] enabled = true [assets] directory = "dist/client" binding = "ASSETS" # Worker を先に走らせて bot UA → OGP HTML 差し込みを可能にする。 # 通常 UA のときは Work
- architecture ↗
name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"
- architecture ↗
name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enab
- architecture ↗
ASSETS.fetch(req) を呼んで # SPA を返す。これが無いと static asset path が Worker をバイパスして # 直接配信され、bot 判定が効かない。 run_worker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib
- architecture ↗
orker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきに
- architecture ↗
images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきにくい壊れ方をするので要注意。 # 実装: apps/worker/src/durable-objects/tenant-scheduler.ts # ═══════════════════════════════════════════════════════════════ [[durable_objects.bindings]] name = "TENANT_SCHEDULER" cl
Upstream screenshot · Shudesu/line-harness-oss repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
LINE Official Account contact management, tagging, operator chat and messaging workflows; no complete hosted LINE marketing-service parity or migration claim.
See supporting source ↗How it works
The shape of L Harness on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 53864603502a. Files were read as data; upstream applications and CI jobs were not executed.
Partial source coverage: 116 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.
Deployment configuration · 4 files
Cloudflare Pages · compatibility 2024-12-01
line-crm-liff · default
No resource bindings declared in this scope.
line-crm-liff-prod · env.production
Inherited from default: compatibility_date
No resource bindings declared in this scope.
Cloudflare Workers · compatibility 2024-12-01
line-harness · default
Entrypoint: src/index.ts
Static assets: dist/client · Worker first: true
Cron triggers (UTC): * * * * * · 0 */6 * * *
DB→ D1IMAGES→ R2TENANT_SCHEDULER→ Durable Objects · class TenantSchedulerASSETS→ Static assets
line-harness · env.production
Inherited from default: main, compatibility_date
Entrypoint: src/index.ts
Static assets: dist/client · Worker first: true
Cron triggers (UTC): * * * * * · 0 */6 * * *
DB→ D1IMAGES→ R2TENANT_SCHEDULER→ Durable Objects · class TenantSchedulerASSETS→ Static assets
Cloudflare Workers · example/template, excluded from overview · compatibility 2026-09-07
line-harness-plugin-tag-rules · default
Entrypoint: src/index.ts
No resource bindings declared in this scope.
Cloudflare Workers · compatibility 2024-06-01
line-harness-plugin-myservice · default
Entrypoint: src/index.ts
No resource bindings declared in this scope.
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L975 · fetch handler exported
- L976 · scheduled handler exported
- L165 · app.use("*")
- L170 · app.use("/api/public/media-inquiries")
- L186 · app.use("*")
- L195 · app.use("*")
- L198 · app.use("*")
- L201 · app.route("/")
- L202 · app.route("/")
- L203 · app.route("/")
- L204 · app.route("/")
- L205 · app.route("/")
- L206 · app.route("/")
- L207 · app.route("/")
- L208 · app.route("/")
- L209 · app.route("/")
- L210 · app.route("/")
- L211 · app.route("/")
- L212 · app.route("/")
- L213 · app.route("/")
- L214 · app.route("/")
- L215 · app.route("/")
- L216 · app.route("/")
- L217 · app.route("/")
- L218 · app.route("/")
- L221 · app.route("/")
- L222 · app.route("/")
- L223 · app.route("/")
- L224 · app.route("/")
- L225 · app.route("/")
- L226 · app.route("/")
- L227 · app.route("/")
- L228 · app.route("/")
- L229 · app.route("/")
- L230 · app.route("/")
- L231 · app.route("/")
- L232 · app.route("/")
- L233 · app.route("/")
- L234 · app.route("/")
- L235 · app.route("/")
Environment references: c.env.LIFF_URL · c.env.DB · c.env.ASSETS
- L8 · scheduled handler exported · references LINE_HARNESS_API_KEY, DRY_RUN, LINE_HARNESS_API_URL, LINE_ACCOUNT_ID, TARGET_TAG_ID · calls applyTagRules, console.log, JSON.stringify
- L27 · fetch handler exported · calls Response.json
Environment references: env.LINE_HARNESS_API_KEY · env.DRY_RUN · env.LINE_HARNESS_API_URL · env.LINE_ACCOUNT_ID · env.TARGET_TAG_ID
- L41 · isFiveMinuteTick calls (conditional paths may differ): getUTCMinutes
- L51 · isHourlyTick calls (conditional paths may differ): getUTCMinutes
- L64 · scheduled calls (conditional paths may differ): getLineAccounts, lineClients.set, Promise.allSettled, recoverStalledBroadcasts, recoverStuckDeliveries, refreshLineAccessTokens, console.error, processDueReminders, console.log, processDueEventReminders, processDueMeetConsultationReminders, Promise.resolve, lineProxy.fetch, exec, processWebinarReminders, processWebinarFollowups, jobs.push, startBulkSendJobs, processQueuedBroadcasts, processScheduledBroadcasts
Environment references: env.DB · env.LINE_CHANNEL_ACCESS_TOKEN · env.WORKER_PUBLIC_URL · env.LIFF_URL · env.WORKER_URL · env.IMAGES
- L46 · nextMinuteBoundary calls (conditional paths may differ): Math.floor
- L58 · isSixHourBoundary calls (conditional paths may differ): now.getUTCMinutes, now.getUTCHours
- L95 · runSchedulerTick calls (conditional paths may differ): storage.setAlarm, nextMinuteBoundary, now.getTime, runJobs, isSixHourBoundary, console.error
- L126 · ensureAlarmArmed calls (conditional paths may differ): storage.getAlarm, storage.setAlarm, nextMinuteBoundary, now.getTime
- L177 · ensureSchedulerArmed calls (conditional paths may differ): ns.get, ns.idFromName, stub.ensureArmed, console.error
Environment references: env.TENANT_SCHEDULER
- L16 · parseCookieHeader calls (conditional paths may differ): cookieHeader.split, split, part.trim, safeDecode, rawValue.join
- L27 · bearerToken calls (conditional paths may differ): c.req.header, authHeader.startsWith, authHeader.slice
- L33 · cookieToken calls (conditional paths may differ): parseCookieHeader, c.req.header
- L37 · csrfTokenFromCookie calls (conditional paths may differ): parseCookieHeader, c.req.header
- L41 · buildCookie calls (conditional paths may differ): encodeURIComponent, parts.push, parts.join
- L55 · adminSessionCookie calls (conditional paths may differ): buildCookie
- L67 · csrfCookie calls (conditional paths may differ): buildCookie
- L71 · expiredCookie calls (conditional paths may differ): buildCookie
- L86 · authenticateApiToken calls (conditional paths may differ): getStaffByApiKey, console.log
- L119 · authMiddleware calls (conditional paths may differ): c.req.method.toUpperCase, path.startsWith, next, test, bearerToken, cookieToken, authenticateApiToken, c.set, path.match, c.json, SAFE_METHODS.has, c.req.header, csrfTokenFromCookie
Environment references: c.env.DB · c.env.API_KEY · c.env.LEGACY_API_KEY
- L26 · prune calls (conditional paths may differ): Date.now, entry.timestamps.filter, store.delete
- L37 · check calls (conditional paths may differ): Date.now, prune, store.get, store.set, entry.timestamps.filter, Math.ceil, Math.max, entry.timestamps.push
- L71 · isUnauthenticatedPath calls (conditional paths may differ): UNAUTHENTICATED_PATTERNS.some, p.test
- L77 · getClientIp calls (conditional paths may differ): c.req.header
- L86 · getAdminCookieToken calls (conditional paths may differ): c.req.header, cookie.split, split, part.trim, rawValue.join, decodeURIComponent
- L124 · rateLimitMiddleware calls (conditional paths may differ): path.startsWith, next, isUnauthenticatedPath, getClientIp, c.req.header, authHeader.slice, getAdminCookieToken, token.slice, check, c.json, String, c.header
- L77 · webhook.post("/webhook")
- L37 · ensureFriendFromWebhookUser calls (conditional paths may differ): getFriendByLineUserId, lineClient.getProfile, console.error, upsertFriend, console.log, jstNow, run, bind, db.prepare
- L198 · handleEvent calls (conditional paths may differ): console.log, lineClient.getProfile, console.error, upsertFriend, run, bind, db.prepare, jstNow, awardActivityMileage, setTimeout, getFriendById, getEntryRouteByRefCode, getScenarios, enrollFriendInScenario, pushImmediateFirstStep, getMessageTemplateById, buildMessage, lineClient.pushMessage, fireEvent, updateFriendFollowStatus
Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.LINE_CHANNEL_SECRET · c.env.WORKER_URL · c.env.LIFF_URL · c.env.IMAGES
- L87 · friends.get("/api/friends")
- L348 · friends.get("/api/friends/count")
- L367 · friends.get("/api/friends/ref-stats")
- L393 · friends.get("/api/friends/:id/mileage")
- L417 · friends.get("/api/friends/:id")
- L454 · friends.post("/api/friends/:id/tags")
- L491 · friends.delete("/api/friends/:id/tags/:tagId")
- L509 · friends.put("/api/friends/:id/metadata")
- L552 · friends.get("/api/friends/:id/messages")
- L577 · friends.post("/api/friends/:id/messages")
- L33 · serializeFriend calls (conditional paths may differ): Boolean, JSON.parse
- L57 · serializeFriendListRow calls (conditional paths may differ): serializeFriend
Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL
- L34 · tags.get("/api/tags")
- L48 · tags.patch("/api/tags/:id/mileage")
- L95 · tags.post("/api/tags")
- L123 · tags.delete("/api/tags/:id")
- L15 · serializeTag calls (conditional paths may differ): Number
Environment references: c.env.DB
- L177 · scenarios.get("/api/scenarios")
- L213 · scenarios.get("/api/scenarios/:id")
- L236 · scenarios.post("/api/scenarios")
- L285 · scenarios.put("/api/scenarios/:id")
- L321 · scenarios.delete("/api/scenarios/:id")
- L333 · scenarios.post("/api/scenarios/:id/steps")
- L412 · scenarios.put("/api/scenarios/:id/steps/:stepId")
- L577 · scenarios.delete("/api/scenarios/:id/steps/:stepId")
- L589 · scenarios.post("/api/scenarios/:id/steps/reorder")
- L666 · scenarios.get("/api/scenarios/:id/preview")
- L757 · scenarios.get("/api/scenarios/:id/stats")
- L784 · scenarios.get("/api/scenarios/:id/enrollments")
- L875 · scenarios.post("/api/scenarios/:id/enroll/:friendId")
- L32 · serializeScenario calls (conditional paths may differ): Boolean
- L79 · validateStepCondition calls (conditional paths may differ): isSupportedConditionType, String, SUPPORTED_CONDITION_TYPES.join, JSON.parse, Array.isArray
- L124 · validateStepSchedule calls (conditional paths may differ): HHMM_RE.test
Environment references: c.env.DB
- L145 · broadcasts.get("/api/broadcasts")
- L157 · broadcasts.get("/api/broadcasts/:id")
- L177 · broadcasts.get("/api/broadcasts/:id/preview-count")
- L244 · broadcasts.get("/api/broadcasts/:id/per-account-stats")
- L333 · broadcasts.post("/api/broadcasts")
- L429 · broadcasts.put("/api/broadcasts/:id")
- L523 · broadcasts.delete("/api/broadcasts/:id")
- L572 · broadcasts.post("/api/broadcasts/:id/send")
- L836 · broadcasts.post("/api/broadcasts/:id/send-segment")
- L932 · broadcasts.get("/api/broadcasts/:id/insight")
- L964 · broadcasts.post("/api/broadcasts/:id/fetch-insight")
- L1129 · broadcasts.post("/api/broadcasts/:id/test-send")
- L1215 · broadcasts.get("/api/broadcasts/:id/progress")
- L1233 · broadcasts.post("/api/segments/count")
- L40 · unsupportedVariablesError calls (conditional paths may differ): getUnsupportedBroadcastVariables, join, unsupported.map
- L54 · parseJsonArray calls (conditional paths may differ): Array.isArray, JSON.parse
- L66 · parseJsonObject calls (conditional paths may differ): JSON.parse, Array.isArray
- L94 · sameCreateRequest calls (conditional paths may differ): parseJsonArray, JSON.stringify
- L110 · serializeBroadcast calls (conditional paths may differ): parseJsonObject, parseJsonArray
- L551 · isLockedUsageRecord calls (conditional paths may differ): quotaConfig, monthStartJst
Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL
- L38 · users.get("/api/users")
- L49 · users.get("/api/users/:id")
- L64 · users.post("/api/users")
- L82 · users.put("/api/users/:id")
- L110 · users.delete("/api/users/:id")
- L121 · users.post("/api/users/:id/link")
- L139 · users.get("/api/users/:id/accounts")
- L159 · users.post("/api/users/match")
- L19 · logUsersRouteError calls (conditional paths may differ): console.error
Environment references: c.env.DB
- L99 · lineAccounts.get("/api/line-accounts")
- L193 · lineAccounts.get("/api/line-accounts/delivery-health")
- L303 · lineAccounts.get("/api/line-accounts/:id")
- L321 · lineAccounts.get("/api/line-accounts/:id/follower-insight")
- L356 · lineAccounts.get("/api/line-accounts/:id/follower-import")
- L363 · lineAccounts.post("/api/line-accounts/:id/follower-import/detect")
- L385 · lineAccounts.post("/api/line-accounts/:id/follower-import/start")
- L403 · lineAccounts.post("/api/line-accounts/:id/follower-import/step")
- L512 · lineAccounts.post("/api/line-accounts")
- L626 · lineAccounts.patch("/api/line-accounts/order")
- L666 · lineAccounts.patch("/api/line-accounts/:id")
- L786 · lineAccounts.put("/api/line-accounts/:id")
- L894 · lineAccounts.delete("/api/line-accounts/:id")
- L27 · serializeLineAccount calls (conditional paths may differ): Boolean
- L52 · serializeLineAccountFull calls (conditional paths may differ): serializeLineAccount
- L62 · maskSecret calls (conditional paths may differ): value.slice
- L75 · serializeLineAccountMasked calls (conditional paths may differ): serializeLineAccount, maskSecret
- L85 · fetchBotProfile calls (conditional paths may differ): fetch, res.json
- L150 · getFollowersInsightCached calls (conditional paths may differ): encodeURIComponent, cache.match, hit.json, client.getFollowersInsight, cache.put, JSON.stringify
- L430 · normalizeOptionalString calls (conditional paths may differ): v.trim
- L481 · checkUniqueLoginAndLiff calls (conditional paths may differ): checks.push, first, bind, db.prepare
Environment references: c.env.DB
- L24 · conversions.get("/api/conversions/points")
- L44 · conversions.post("/api/conversions/points")
- L74 · conversions.delete("/api/conversions/points/:id")
- L87 · conversions.post("/api/conversions/track")
- L131 · conversions.get("/api/conversions/events")
- L162 · conversions.get("/api/conversions/report")
- L183 · conversions.get("/api/conversions/approvals")
- L211 · conversions.patch("/api/conversions/events/:id/approval")
Environment references: c.env.DB
- L40 · affiliates.get("/api/affiliates")
- L51 · affiliates.get("/api/affiliates/:id")
- L77 · affiliates.post("/api/affiliates")
- L198 · affiliates.put("/api/affiliates/:id")
- L224 · affiliates.delete("/api/affiliates/:id")
- L237 · affiliates.get("/api/affiliates/:id/report")
- L257 · affiliates.get("/api/affiliates/:id/journeys")
- L277 · affiliates.get("/api/affiliates/:id/links")
- L300 · affiliates.get("/api/friends/:id/journey")
- L311 · affiliates.post("/api/affiliates/click")
- L337 · affiliates.get("/api/affiliates-report")
- L27 · serializeAffiliate calls (conditional paths may differ): Boolean
Environment references: c.env.DB
- L42 · affiliateOffers.get("/api/affiliate-offers")
- L54 · affiliateOffers.get("/api/affiliate-offers/:id")
- L68 · affiliateOffers.post("/api/affiliate-offers")
- L116 · affiliateOffers.put("/api/affiliate-offers/:id")
- L20 · serializeOffer calls (conditional paths may differ): Boolean
- L37 · isValidReward calls (conditional paths may differ): Number.isInteger
Environment references: c.env.DB
- L70 · duplicates.get("/api/duplicates/stats")
- L54 · serializeDuplicatesStats calls (conditional paths may differ): stats.per_account.map, stats.pairwise_overlap.map
Environment references: c.env.DB
- L8 · usersGrouped.get("/api/users-grouped")
Environment references: c.env.DB
- L18 · inbox.get("/api/inbox/activity-digest")
- L36 · inbox.get("/api/inbox/unanswered")
- L60 · inbox.get("/api/inbox/unanswered/count")
Environment references: c.env.DB
- L317 · liffRoutes.get("/auth/line")
- L572 · liffRoutes.get("/auth/oauth")
- L652 · liffRoutes.get("/auth/callback")
- L1107 · liffRoutes.get("/api/liff/config")
- L1151 · liffRoutes.post("/api/liff/profile")
- L1179 · liffRoutes.post("/api/liff/link")
- L1405 · liffRoutes.get("/api/analytics/ref-summary")
- L1478 · liffRoutes.get("/api/analytics/ref/:refCode")
- L1536 · liffRoutes.post("/api/links/wrap")
- L1787 · liffRoutes.post("/api/liff/send-form-link")
- L47 · encodeState calls (conditional paths may differ): btoa, String.fromCharCode, encode
- L50 · decodeState calls (conditional paths may differ): decode, Uint8Array.from, atob, ch.charCodeAt
- L64 · linkIgIgsid calls (conditional paths may differ): run, bind, c.env.DB.prepare, first, console.error, console.warn, awardActivityMileage, c.executionCtx.waitUntil, catch, then, fetch, JSON.stringify, res.text
- L146 · saveIgAccountMeta calls (conditional paths may differ): first, bind, db.prepare, JSON.parse, igAccountUsername.replace, run, JSON.stringify, console.error
- L199 · applyRefAttribution calls (conditional paths may differ): ref.startsWith, isReservedRef, getEntryRouteByRefCode, getTrackedLinkById, getAffiliateLinkByRefCode, getAffiliateById, getAffiliateOfferById, notifyAffiliateFriendAdd, console.error, attachTagAndFireSideEffects, pushImmediateFirstStep
- L1563 · authLandingPage calls (conditional paths may differ): liffUrl.match, liffUrl.indexOf, liffUrl.slice, escapeHtml
- L1638 · completionPage calls (conditional paths may differ): escapeHtml
- L1673 · errorPage calls (conditional paths may differ): escapeHtml
- L1697 · escapeHtml calls (conditional paths may differ): replace, str.replace
- L1707 · applyXHarnessActions calls (conditional paths may differ): first, bind, db.prepare, crypto.randomUUID, jstNow, addTagToFriend, console.log, console.error, enrollFriendInScenario
- L1760 · resolveXHarnessToken calls (conditional paths may differ): setTimeout, controller.abort, fetch, res.json, clearTimeout
Environment references: c.env.DB · c.env.IG_HARNESS_URL · c.env.IG_HARNESS_LINK_SECRET · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL · c.env.LINE_LOGIN_CHANNEL_ID · c.env.LIFF_URL · c.env.LINE_LOGIN_CHANNEL_SECRET · c.env.LINE_CHANNEL_SECRET · env.X_HARNESS_URL
- L167 · affiliateSelfRoutes.get("/api/liff/mileage/me")
- L213 · affiliateSelfRoutes.post("/api/liff/affiliate/register")
- L283 · affiliateSelfRoutes.get("/api/liff/affiliate/me")
- L330 · affiliateSelfRoutes.post("/api/liff/affiliate/links")
- L403 · affiliateSelfRoutes.get("/api/liff/affiliate/offers")
- L462 · affiliateSelfRoutes.post("/api/liff/affiliate/offers/:id/enroll")
- L54 · resolveFriendFromLineToken calls (conditional paths may differ): fetch, encodeURIComponent, catch, v.json, allowedChannelIds.add, getLineAccounts, allowedChannelIds.has, prof.json, getFriendByLineUserId
- L103 · unresolvedResponse calls (conditional paths may differ): c.json
- L145 · loadOfferNames calls (conditional paths may differ): listAffiliateOffers, offers.map
- L150 · serializeAffiliate calls (conditional paths may differ): Boolean
Environment references: env.DB · env.LINE_LOGIN_CHANNEL_ID · c.env.DB · c.env.LINE_CHANNEL_SECRET
Build and deployment pipeline · 9 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: pull_request, push, workflow_dispatch
worker · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-harness/update-engine build - Shell command
pnpm --filter worker typecheck - Shell command
pnpm --filter worker test - Shell command
pnpm --filter worker build
setup-and-upgrade · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - Ensure historical schema fixtures are available
set -euo pipefail for tag in v0.14.1 v0.15.0 v0.16.0 v0.17.0 v0.18.0 v0.18.1; do if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then git fetch --depth=1 --no-tags https://github.com/Shudesu/line-harness-oss.git "refs/tags/$tag:refs/tags/$tag" fi done - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-harness/update-engine build - Shell command
pnpm --filter @line-crm/db test - Shell command
pnpm --filter @line-crm/db typecheck - Shell command
pnpm --filter @line-harness/update-engine test - Shell command
pnpm --filter @line-harness/update-engine typecheck - Shell command
pnpm --filter create-line-harness test - Shell command
pnpm --filter create-line-harness build - Shell command
pnpm --filter create-line-harness exec tsc --noEmit - Shell command
pnpm exec tsx scripts/check-migrations.ts - Shell command
node packages/db/scripts/generate-bootstrap.mjs --check
Triggers: workflow_dispatch, push
deploy · no job dependencies declared
Condition: github.repository != 'Shudesu/line-harness-oss' && vars.LINE_HARNESS_CLOUDFLARE_DEPLOY == 'true'
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-crm/shared --filter @line-harness/update-engine build - Capture build metadata
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" - Build Admin Panel
pnpm --filter web build - Deploy to Cloudflare Pages
test -n "$CLOUDFLARE_API_TOKEN" test -n "$CLOUDFLARE_ACCOUNT_ID" test -n "$NEXT_PUBLIC_API_URL" npx wrangler pages deploy apps/web/out \ --project-name="$PAGES_PROJECT_NAME" \ --commit-message="deploy ${GITHUB_SHA}"
Triggers: workflow_dispatch, push
deploy · no job dependencies declared
Condition: github.repository != 'Shudesu/line-harness-oss' && vars.LINE_HARNESS_CLOUDFLARE_DEPLOY == 'true'
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine build - Run pending D1 migrations
test -n "$CLOUDFLARE_API_TOKEN" test -n "$CLOUDFLARE_ACCOUNT_ID" test -n "$D1_DATABASE_NAME" npx wrangler d1 execute "$D1_DATABASE_NAME" --remote --command \ "CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)" for f in $(ls packages/db/migrations/*.sql | sort); do name=$(basename "$f") applied=$(npx wrangler d1 execute "$D1_DATABASE_NAME" --remote \ --command "SELECT name FROM _migrations WHERE name = '${name}'" --json \ | node -e "const d=JSON.parse(requi… - Build Worker and LIFF assets
pnpm --filter worker build - Patch wrangler config
test -n "$D1_DATABASE_ID" cd apps/worker sed -i \ -e "s|\"account_id\":\"[^\"]*\"|\"account_id\":\"${CLOUDFLARE_ACCOUNT_ID}\"|g" \ -e "s|\"database_name\":\"[^\"]*\"|\"database_name\":\"${D1_DATABASE_NAME}\"|g" \ -e "s|\"database_id\":\"[^\"]*\"|\"database_id\":\"${D1_DATABASE_ID}\"|g" \ dist/line_harness/wrangler.json # Inject admin CORS vars into the deployed config when ADMIN_ORIGIN is # set. ADMIN_ALLOW_CROSS_SITE defaults to "true" (SameSite=None; Secure # cookies) since cross-origin admin… - Deploy to Cloudflare Workers
cloudflare/wrangler-action@v3Wrangler command: deploy --config dist/line_harness/wrangler.json --name ${{ vars.WORKER_NAME || 'your-worker-name' }}
Triggers: push, workflow_dispatch
deploy · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - actions/configure-pages@v5
actions/configure-pages@v5 - actions/upload-pages-artifact@v3
actions/upload-pages-artifact@v3 - actions/deploy-pages@v4
actions/deploy-pages@v4
Triggers: pull_request, push, workflow_dispatch
market-web · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-crm/shared --filter @line-harness/update-engine build - Shell command
pnpm exec vitest run scripts/create-plugin.test.ts scripts/plugin-tag-rules.test.ts - Shell command
pnpm --filter web test - Shell command
pnpm --filter web build
plugin-starters · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - actions/setup-node@v4
actions/setup-node@v4 - Generate outside the main checkout
node examples/plugins/create-plugin.mjs "$RUNNER_TEMP/plugin-smoke" "${{ matrix.template }}" - Install and build the independent plugin
npm install --no-audit --no-fund npm run typecheck npm run build npm run build:mcp --if-present
Triggers: workflow_dispatch
publish · no job dependencies declared
- actions/checkout@v6
actions/checkout@v6 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v6
actions/setup-node@v6 - Install dependencies and current npm CLI
npm install --global npm@11 pnpm install --frozen-lockfile - Test and build publishable packages
pnpm --filter @line-harness/update-engine test pnpm --filter @line-harness/update-engine build pnpm --filter create-line-harness test pnpm --filter create-line-harness build - Pack and publish with npm OIDC
pack_dir="$RUNNER_TEMP/line-harness-packs" mkdir -p "$pack_dir" update_version=$(node -p "require('./packages/update-engine/package.json').version") cli_version=$(node -p "require('./packages/create-line-harness/package.json').version") pnpm --dir packages/update-engine pack --pack-destination "$pack_dir" pnpm --dir packages/create-line-harness pack --pack-destination "$pack_dir" if npm view "@line-harness/update-engine@$update_version" version >/dev/null 2>&1; then echo "@line-harness/update-e…
Triggers: push
release · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Resolve version from tag
# Strip the leading "v" — manifest entries use plain semver. tag="${GITHUB_REF_NAME}" version="${tag#v}" echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" - Migration safety check
pnpm tsx scripts/check-migrations.ts - Run unit tests (scripts)
pnpm test:scripts - Build shared workspace packages
pnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine --filter @line-harness/sdk build - Run every workspace typecheck
pnpm -r --workspace-concurrency=1 --if-present run typecheck - Run every populated workspace test suite
pnpm --filter @line-crm/db test pnpm --filter @line-harness/update-engine test pnpm --filter create-line-harness test pnpm --filter worker test pnpm --filter web test pnpm --filter liff test - Capture build metadata
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" - Build Admin (Next.js static export)
pnpm --filter web build - Build LIFF
pnpm --filter liff build - Build Worker Assets
pnpm --filter worker build - Write CI wrangler config for artifact builds
cat > apps/worker/wrangler.ci.toml <<'TOML' name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] TOML - Build Worker artifact (first pass — for identity hashing)
pnpm --filter worker exec wrangler deploy --dry-run \ --config wrangler.ci.toml --outdir dist-release - Compute hashes + inject _version.ts
# Hash the wrangler-built single-file artifact. This value is # baked into _version.ts as the Worker's self-reported identity # (worker_hash); the final artifact's byte hash is computed # separately as worker_bundle_hash after the rebuild. json=$(pnpm tsx apps/worker/scripts/inject-version.ts \ --version "${{ steps.version.outputs.version }}" \ --worker apps/worker/dist-release/index.js \ --worker-assets apps/worker/dist/client \ --admin apps/web/out \ --liff apps/liff/dist \ --out apps/worker/… - Rebuild Worker artifact (with embedded _version.ts)
rm -rf apps/worker/dist-release pnpm --filter worker exec wrangler deploy --dry-run \ --config wrangler.ci.toml --outdir dist-release - Compute worker_bundle_hash (detached hash of the final artifact)
worker_bundle_hash=$(node -e "const{createHash}=require('node:crypto');const{readFileSync}=require('node:fs');console.log('sha256:'+createHash('sha256').update(readFileSync('apps/worker/dist-release/index.js')).digest('hex'))") echo "worker_bundle_hash=${worker_bundle_hash}" >> "$GITHUB_OUTPUT" - Build release bundle
mkdir -p dist pnpm tsx scripts/release/build-bundle.ts \ --worker-js apps/worker/dist-release/index.js \ --worker-assets apps/worker/dist/client \ --admin apps/web/out \ --liff apps/liff/dist \ --migrations packages/db/migrations \ --out dist/bundle.tar.gz - Compute bundle size
size=$(stat -c%s dist/bundle.tar.gz) echo "size=${size}" >> "$GITHUB_OUTPUT" - Determine cumulative migrations for safe upgrades
# Include the supported additive bridge from 037 onward. v0.14.1's # base+setup already establishes 001-036, while two older files use # destructive table rebuilds and must never be replayed statement by # statement. The checksum ledger makes this cumulative set safe. migrations_json=$(ls packages/db/migrations \ | awk '/^03[7-9]_|^0[4-9][0-9]_|^[1-9][0-9][0-9]_/' \ | jq -R . | jq -sc .) if [ -z "$migrations_json" ] || [ "$migrations_json" = "null" ]; then migrations_json="[]" fi echo "list=${m… - Generate release-entry.json
bundle_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ steps.version.outputs.tag }}/bundle.tar.gz" changelog_url="https://github.com/${GITHUB_REPOSITORY}/blob/${{ steps.version.outputs.tag }}/CHANGELOG.md" # min_from_version: 0.0.0 for the first release so the upgrade # flow accepts any prior install. Bump for breaking releases # that require a prior version. cat > release-entry.json <<JSON { "version": "${{ steps.version.outputs.version }}", "legacy_mileage_projection_versio… - Fetch previous release-manifest.json (if any)
# Pull the manifest from the most recent published release that # isn't this tag. If none exists, start fresh — update-manifest.ts # initialises { schema_version: 1, latest, releases: [] } on first # write. prev_tag=$(gh release list --limit 5 --json tagName --jq '.[].tagName' 2>/dev/null \ | grep -v "^${{ steps.version.outputs.tag }}$" \ | head -n 1 || true) if [ -n "$prev_tag" ]; then echo "Pulling manifest from previous release: $prev_tag" gh release download "$prev_tag" --pattern 'release-m… - Update release-manifest.json
pnpm tsx scripts/release/update-manifest.ts \ --manifest release-manifest.json \ --release release-entry.json cat release-manifest.json - Publish draft GitHub Release
softprops/action-gh-release@v2
Triggers: workflow_dispatch, schedule
update · no job dependencies declared
Condition: github.repository != 'Shudesu/line-harness-oss'
- actions/checkout@v4
actions/checkout@v4 - Configure git
git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - Fetch upstream
git remote add upstream https://github.com/Shudesu/line-harness-oss.git git fetch upstream main - Detect upstream changes
if git merge-base --is-ancestor upstream/main HEAD; then echo "has_changes=false" >> "$GITHUB_OUTPUT" else echo "has_changes=true" >> "$GITHUB_OUTPUT" fi - Create update branch
branch="upstream/update-$(date -u +%Y%m%d-%H%M%S)" git switch -c "$branch" echo "name=$branch" >> "$GITHUB_OUTPUT"Condition: steps.detect.outputs.has_changes == 'true' - Merge upstream/main
git merge --no-edit upstream/mainCondition: steps.detect.outputs.has_changes == 'true' - Push update branch
git push origin "${{ steps.branch.outputs.name }}"Condition: steps.detect.outputs.has_changes == 'true' - Open pull request
gh pr create \ --base main \ --head "${{ steps.branch.outputs.name }}" \ --title "chore: update from upstream" \ --body "This PR imports the latest Shudesu/line-harness-oss main into this fork.\n\nMerge this PR to receive upstream fixes and features, then let the existing Cloudflare deploy workflows update your environment."Condition: steps.detect.outputs.has_changes == 'true' - No upstream changes
echo "Fork is already up to date with upstream/main."Condition: steps.detect.outputs.has_changes == 'false'
Triggers: pull_request, push, workflow_dispatch
worker-ci · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine build - Shell command
pnpm --filter worker typecheck - Shell command
pnpm --filter worker test - Shell command
pnpm --filter worker build
build: pnpm -r builddeploy:worker: pnpm --filter worker deploydeploy:web: pnpm --filter web builddeploy:setup: node packages/create-line-harness/dist/index.jsdeploy:update: node packages/create-line-harness/dist/index.js updatebuild:bundle: tsx scripts/release/build-bundle.tsbuild:wfp-bundle: tsx scripts/release/build-wfp-bundle.ts
build: tsc -b && vite builddeploy: pnpm build && wrangler pages deploy dist
build: next build
build: vite builddeploy: vite build && wrangler deploy
build: wrangler deploy --dry-rundeploy: wrangler deploy
build: tsupprepublishOnly: node ./scripts/guard-source-publish.mjs
build: tsc -p tsconfig.build.jsonprepublishOnly: npm run build
build: tsup
build: tsup src/index.ts --format esm --dtsbuild:mcp: tsup mcp-server/index.ts --format esm --out-dir dist-mcpdeploy: wrangler deploy
build: tsup
build: tsc -p tsconfig.build.jsonprepublishOnly: npm run build
build: tsup && node scripts/preserve-node-builtins.mjs
Repository README
View original on GitHub ↗Full upstream document by @Shudesu · README.md · snapshot 5386460
🌐 日本語 | English | 简体中文 | 한국어 | Español
L Harness
名称について: 2026年8月19日から製品表示名を「L Harness」に統一しました。 既存導入を壊さないため、リポジトリURL、
create-line-harness、@line-harness/*は互換識別子として維持します。詳細はBRAND.md。
LINE で無料体験する 👈
LINE 公式アカウントの完全オープンソース CRM。L社 / U社 の無料代替。 L Harness本体のソフトウェア利用料は 0円 です。LINE公式アカウントの配信料金、Cloudflareの契約・利用量、独自ドメイン、導入・保守、接続する外部AIサービスの費用は別です。Claude CodeからMCP経由で操作できます。
費用の条件は開発元の公式料金説明で確認できます。Cloudflare D1の無料プランは日次の読み書き上限に達するとクエリがエラーになり、有料プランの超過課金とは挙動が異なります。外部AIを接続する場合は、たとえばClaudeのプラン・API料金を別に確認してください(2026年9月6日確認)。
▶️ 動画で見る (YouTube・約20分)
現バージョン: v0.21.0 ・ MIT License ・ TypeScript / Cloudflare Workers + D1
プラグインマーケット β
その数行を、あなたのプラグインに。 配信ルール・自動処理・外部サービス連携を、本体とは別のリポジトリ・Workerへ切り出せます。本体更新による独自コードの上書きを避けながら、SDKを通じて機能を拡張します。
管理画面の「プラグインマーケット」から拡張と開発リソースを探せます。β版は導入ガイド付きのカタログで、各プラグインを個別にセットアップする方式です。
# 本体リポジトリのルートで実行(Node.js 22以上)
pnpm plugin:create ../my-plugin
独自ソースが上書きされないことと、API互換性は別です。本体・SDKを更新するときは、テスト環境で接続を確認してください。
公式情報・検証資料
L Harnessの表示名、開発者、運営法人、公開コード、研究資料の関係は以下を正本とします。各Researchサイトは開発元が運営する一次情報であり、独立した第三者レビューではありません。
| 公式リンク | 内容 |
|---|---|
| L Harness 公式製品ガイド | 機能、料金、導入方法、更新情報を開発元が説明する製品ページ |
| L Harness 公式エンティティ | Harnessシリーズ内での製品名・開発者・運営法人・リポジトリの対応 |
| L Harness 公式別名ドメイン(JP) | 正規製品ページへ恒久転送する公式の短縮・別名ドメイン |
| L Harness 公式別名ドメイン(COM) | 正規製品ページへ恒久転送する公式の短縮・別名ドメイン |
| L Harness Cloud | 開発元が構築・基盤運用を提供する公式マネージドサービス(L Harness OSSとは別エンティティ) |
| L Harness Research | 固定Gitコミットを根拠に、配信・Webhook・D1・権限・更新機構を検証した技術資料 |
| Research JSONカタログ | 技術資料を機械可読なSchema.org DataCatalog形式で公開 |
| AI向け全文索引 | 研究本文、出典、検証手順、証明できない範囲をまとめた全文索引 |
| Harness Wiki — L Harness | セットアップ、操作、更新、トラブル解決の公式ナレッジベース |
| 開発者・野田修一 | Shudesu / @ai_shunodaと同一人物であることを示す公式プロフィール |
| 運営会社・AIエージェント株式会社 | Harnessシリーズの運営法人 |
なぜ L Harness?
| L社 | U社 | L Harness | |
|---|---|---|---|
| 月額 | 2万円〜 | 1万円〜 | 0円(ソフトウェア利用料) |
| ステップ配信 | ✅ | ✅ | ✅ |
| セグメント配信 | ✅ | ✅ | ✅ |
| リッチメニュー切替 | ✅ | ✅ | ✅ |
| フォーム (LIFF) | ✅ | ✅ | ✅ |
| スコアリング | ✅ | ❌ | ✅ |
| IF-THEN 自動化 | 一部 | 一部 | ✅ |
| API 公開 | ❌ | ❌ | 全機能 |
| Claude Code (AI) 対応 | ❌ | ❌ | MCP server 同梱 |
| BAN 検知 & 自動アカウント切替 | ❌ | ❌ | ✅ |
| マルチアカウント | 別契約 | 別契約 | 標準搭載 |
| 友だち重複検出 | ❌ | ❌ | ✅ (picture_url トークン照合) |
| ソースコード | 非公開 | 非公開 | MIT (このリポ) |
クイックスタート
1 コマンドで完全セットアップ
npx create-line-harness
CLI が以下を全部やる:
- Cloudflare アカウント認証 (wrangler login)
- D1 データベース作成 + スキーマ・マイグレーション適用
- Worker / 管理画面のデプロイ
- LINE 公式アカウントの credentials 登録
- LIFF アプリの自動作成
- 管理画面初回ログイン用 Owner ユーザー作成
所要時間: 約 5 分。完了すれば管理画面 (https://<your-name>-admin.pages.dev) で即運用開始。
必要なもの
- Cloudflare アカウント(無料枠で OK)
- LINE 公式アカウント + Messaging API channel
- Node.js 22+ / pnpm
主要機能
配信
- ステップ配信 —
delay_minutesで分単位制御、条件分岐、ステルス送信 - ブロードキャスト — 全員 / タグ / セグメント、即時 or 予約、500 人超は自動キュー化
- リマインダー — 指定日時からのカウントダウン配信(セミナー 3 日前 / 1 日前 / 当日)
- テンプレート —
{{name}}{{uid}}{{auth_url:CHANNEL_ID}}で個別パーソナライズ - トラッキングリンク — クリック計測 → 自動タグ付け → シナリオ起動
CRM
- 友だち管理 — Webhook 自動登録、プロフィール取得、カスタムメタデータ
- タグ — 配信条件・シナリオトリガー
- スコアリング — 行動ベースのリードスコア自動計算
- オペレーターチャット — 管理画面から直接 1:1 返信
- Conversation Inbox — 未返信の会話を放置時間順で一覧(自動配信は除外判定)
- 重複検出 —
picture_url中間トークンで複数アカウント間の同一ユーザーを自動タグ付け
マーケティング
- リッチメニュー — ユーザー別 / タグ別の自動切替
- フォーム (LIFF) — LINE 内完結フォーム、回答 → メタデータ自動保存
- カレンダー予約 — Google Calendar 連携の予約システム (LIFF)
- ライブCTA即時予約 — ウェビナーのフォーム送信後、その場で空き日時を選び、Google Meet発行・LINEリマインドまで自動化
- スタッフ管理 — Owner / Admin / Staff の 3 ロール、API key 個別発行
アフィリエイト計測(ASP)
- アフィリリンク発行 — アフィリエイター自身が LIFF からワンタップで発行、短縮ドメイン対応
- 案件管理 — 案件ごとの固定額報酬を設定、複数案件を並列運用
- 時系列トラッキング — クリック → 友だち追加 → CV を時系列で記録、last-touch 帰属で成果紐づけ
- 成果承認フロー — 帰属 CV を承認/却下して報酬を確定、重複アカウント検知フラグで水増しを可視化
- LINE push 通知 — 成果確定時にアフィリエイターへ自動プッシュ通知
- 詳細: docs/wiki/27-Affiliate-ASP.md
自動化
- IF-THEN ルール — 7 種のトリガー × 6 種のアクション
- 自動返信 — キーワード完全一致 / 部分一致
- Webhook IN/OUT — Stripe / Slack 等の外部サービス連携
- 通知ルール — 条件付きアラート配信
- 配信タイミング —
delay_minutesとscheduled_atで完全制御(v0.13.2 で時間ゲート全廃、運用側ハンドル)
マルチアカウント
- 複数 LINE 公式アカウント を 1 つのダッシュボードで管理
- アカウント別シナリオ・タグ・配信 スコープ
- BAN 検知 → 自動で次のアカウントへ友だち移行(pool 機能)
- トラフィックプール — 複数アカウントへ自動振り分け
AI 統合
- MCP Server 同梱 (
@line-harness/mcp-server) — Claude Code から自然言語で全操作list_conversations/get_conversation— 未返信会話の AI 監視create_scenario/update_step— シナリオを AI に作らせるbroadcast/send_message— メッセージ送信(要ユーザー確認)
- 公式 SDK (
@line-harness/sdk) — TypeScript の型付き SDK、ESM + CJS、ゼロ依存
iOS アプリ対応
GET /api/capabilities— iOS 公式アプリ (the-harness-ios) との互換判定エンドポイント- Owner / Admin / Staff いずれのロールでも利用可能
アーキテクチャ
[ LINE Platform ] ⇄ [ Cloudflare Worker (Hono) ] ⇄ [ D1 SQLite ]
⇅
[ Cloudflare Pages (Next.js 15) ]
⇅
[ MCP Server / SDK / Claude Code ]
- Worker (
apps/worker): API + LIFF + Webhook 受信、cron で配信処理 - Web (
apps/web): Next.js 15 ダッシュボード(19 セクション) - Packages:
@line-harness/sdk— TypeScript SDK@line-harness/mcp-server— Claude Code 用 MCP servercreate-line-harness— セットアップ CLI@line-harness/plugin-template— プラグイン拡張用テンプレート@line-harness/db— D1 マイグレーション + ヘルパー@line-harness/line-sdk— LINE API 薄ラッパー@line-harness/shared— 型定義共有
ドキュメント
- セットアップガイド (動画)
- LINE で無料体験する
- Googleカレンダー連携とライブCTA即時予約
- npm: @line-harness/sdk
- npm: @line-harness/mcp-server
- npm: create-line-harness
ライセンス
MIT License. 商用利用・改変・再配布自由。
コントリビュート
Issue / PR 歓迎。OSS リポへの PR は Shudesu/line-harness-oss (このリポ) に投げてください。
開発者 / Author
野田修一(Shudesu) — Harness シリーズ(L Harness / IG Harness / X Harness)開発者、AIエージェント株式会社 代表
- GitHub: @Shudesu
- X: @ai_shunoda
- YouTube: 野田 修一 | The Harnessで0円
- 公式ドキュメント: Harness Wiki
- 商用ツールとの比較・料金データ: The Harness Lab
L Harness by @Shudesu — AI ネイティブ時代の OSS LINE CRM
Frequently asked about L Harness
What is L Harness?+
L Harness is a self-hosted L Message alternative built on the Cloudflare developer platform. Self-hosted LINE contact CRM, chat and messaging workflows
What does L Harness replace?+
L Harness is listed as an alternative to L Message. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does L Harness use?+
L Harness is built on D1, Durable Objects, Pages, R2, Workers.
How much does L Harness cost to run?+
A small self-hosted CRM can fit the documented Cloudflare free allowances. A LINE Official Account and Messaging API channel are mandatory: The Japan Communication Plan includes up to 200 charge-counted messages/month at no monthly fee; higher-volume delivery incurs LINE plan fees. Optional Claude/AI services and custom domains are separate costs. Operate within the region-specific LINE messaging plan and current delivery allowance; Cloudflare free hosting does not make unlimited LINE sends free. Keep Worker/D1/R2/SQLite-DO use within free quotas and configure your own channel credentials, owner account and LIFF application. External AI is optional. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Check current Cloudflare pricing before deploying.
Is L Harness open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/Shudesu/line-harness-oss/53864603502ae4ccdf350f39a71264b4b41155f2/LICENSE. Source code and contributor credit are available at https://github.com/Shudesu/line-harness-oss.


Discussion · 0
sign in to comment →