Cloudsteading
The breathing page in light mode: an ink blot on paper, 「算了」 as a filled pill, 「继续打开」 as a small underlined link

yixi

A breathing pause before distracting apps, plus a small daily-goal page.

yixi is a self-hosted One Sec alternative built on Cloudflare (D1, Pages, 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

@Defiabell

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply.

Hosting requirements
  • Provide an iPhone with Shortcuts/Safari and configure the app automation; the website alone does not intercept app launches.
  • Use the documented Pages+Worker setup with the same D1 and TOKEN_KEY in both, or an owned custom-domain Worker; replace both upstream database IDs.
  • Keep dynamic requests below the account-wide Workers Free allowance and D1 row/storage use within limits; measure PBKDF2 login CPU against 10 ms.
  • Registration is open and Turnstile is optional; the tool is a user-controlled nudge, not enforced blocking.
  • The two cron ticks and persistent event history consume database usage; custom domain registration is optional/separate.
  • Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured.
  • Dynamic Pages Functions requests share the Workers account allowance; only requests that do not invoke Functions count as free unlimited static asset requests.
  • D1 Free allowance: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; unindexed scans and history retention consume quota.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 2b679f1. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • one-sec ↗

    r and one D1 database, running entirely on Cloudflare's free tier. On the interception side it is a self-hosted stand-in for [One Sec](https://one-sec.app/), built as a web page instead of an iPhone app. <table> <tr> <td width="50%" align="center"> <img src="docs/images/breathing-paper.png" width="300" alt="The breathing page in light mode: an ink blot on paper, 「算了」 as a filled pill, 「继续打开」 as a small underlined link"> <sub>「算了」 is a filled pill, 「继续打开」 a small underlined link — the asymmetry is deliberate.</sub> Before a distracting app opens, the phone jump

  • workers ↗

    name = "yixi" main = "src/index.ts" compatibility_date = "2026-07-01" # One trigger, two daily ticks dispatched by event.scheduledTime (UTC): # 04:00 — noon Shanghai; trim stale sessions, expired logins and rate windows. # 16:00 — midnight Shanghai; snapshot yesterday's goal_days. # Events are never deleted. [triggers] crons = ["0 4,16 * * *"] [observability] enabled = true # Secrets (wrangler secret put ...): # COOKIE_SECRET — legacy; browser sessions are now server-sid

  • d1 ↗

    not read. Every request then 500s with nothing obviously misconfigured. [[d1_databases]] binding = "DB" database_name = "yixi" database_id = "64215db6-0a05-4ae9-a583-19c8790bef06" # Smart Placement: run near the D1 database (APAC) instead of near the # visitor. Mainland-China traffic lands on a US-west colo while D1 sits in # Singapore; every render paid that hop once per query. Decided from live # traffic, may take a while to kick in — check the `cf-placement` header. [placement] mode = "smart"

  • pages ↗

    name = "yixi-app" pages_build_output_dir = "public" compatibility_date = "2026-07-01" # NOTE for anyone cloning this: `database_id` below is the author's own # database. It is not a credential — using it requires an API token for that # account — but you MUST replace it with the id `wrangler d1 create yixi` prints # for you. # # Getting this wrong fails in an unhelpful way: migrations are applied by # database *name*, so they land on your database and appear to succeed, while # the

  • free-tier-eligible ↗

    name = "yixi" main = "src/index.ts" compatibility_date = "2026-07-01" # One trigger, two daily ticks dispatched by event.scheduledTime (UTC): # 04:00 — noon Shanghai; trim stale sessions, expired logins and rate windows. # 16:00 — midnight Shanghai; snapshot yesterday's goal_days. # Events are never deleted. [triggers] crons = ["0 4,16 * * *"] [observability] enabled = true # Secrets (wrangler secret put ...): # COOKIE_SECRET — legacy; browser sessions are

  • free-tier-eligible ↗

    ount Manager. | | Requests<sup>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 second

  • free-tier-eligible ↗

    rs Paid](https://developers.cloudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/obs

  • free-tier-eligible ↗

    s.cloudflare.com/workers/platform/pricing/#how-to-switch-usage-models). ### Static asset requests On both free and paid plans, requests to static assets are free and unlimited. A request is considered static when it does not invoke Functions. Refer to [Functions invocation routes](https://developers.cloudflare.com/pages/functions/routing/#functions-invocation-routes) to learn more about when Functions are invoked. ## Free Plan Requests to your Pages Functions count towards your quota for the Workers Free plan. For example, you could use 50,000 Functions reques

  • MIT ↗

    MIT License Copyright (c) 2026 Defiabell 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

  • architecture ↗

    name = "yixi" main = "src/index.ts" compatibility_date = "2026-07-01" # One trigger, two daily ticks dispatched by event.scheduledTime (UTC): # 04:00 — noon Shanghai; trim stale sessions, expired logins and rate windows. # 16:00 — midnight Shanghai; snapshot yesterday's goal_days. # Events are never deleted. [triggers] crons = ["0 4,16 * * *"] [observability] enabled = true # Secrets (wrangler secret put ...): # COOKIE_SECRET — legacy; browser sessions are now server-sid

  • architecture ↗

    name = "yixi-app" pages_build_output_dir = "public" compatibility_date = "2026-07-01" # NOTE for anyone cloning this: `database_id` below is the author's own # database. It is not a credential — using it requires an API token for that # account — but you MUST replace it with the id `wrangler d1 create yixi` prints # for you. # # Getting this wrong fails in an unhelpful way: migrations are applied by # database *name*, so they land on your database and appear to succeed, while # the

Upstream screenshot · Defiabell/yixi 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.

One Sec logoOne Sec ↗

Editorial workflow alternative: A voluntary breathing interruption triggered by iOS Shortcuts; no native-app enforcement, platform breadth or behavior-change guarantee.

See supporting source ↗
external SaaS target
varies
→ D1 + Pages + Workers

How it works

The shape of yixi 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 ↗
Public interface
Configured entry points2
yixi
wrangler.toml
yixi-app
pages/wrangler.toml
↓
App
yixi
entry
Cloudflare Workers
Entrypoint: src/index.tsConfigured cron (UTC): 0 4,16 * * *
yixi-app
entry
Cloudflare Pages
↓

Configuration and workflow sources

Reviewed commit 2b679f164f44. Files were read as data; upstream applications and CI jobs were not executed.

Partial source coverage: 80 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.

Deployment configuration · 2 files
wrangler.toml ↗

Cloudflare Workers · compatibility 2026-07-01

yixi · default

Entrypoint: src/index.ts

Cron triggers (UTC): 0 4,16 * * *

  • DB → D1
pages/wrangler.toml ↗

Cloudflare Pages · compatibility 2026-07-01

yixi-app · default

  • DB → D1

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.

src/index.ts ↗
  • L55 · fetch handler exported · references DB · calls handleGate, handleResolve, renderBreathe, url.searchParams.get, isLocale, authenticate, setUserLocale, clean.searchParams.delete, sameSitePath, langCookie, renderMock, renderLanding, renderGuide, sitemap, robotsTxt, manifestResponse, iconResponse, surfManifestResponse, surfIconResponse, checkRate, path.slice, tooManyRequests, open, toLogin, handleToday, handleSurf, renderSurfReview, handleSurfSetup, handleGoals, renderTodaySetup, renderProgress, renderReview, handleAccount, handleCandidates, seeOtherTo, temporaryRedirectPreservingMethod, renderSetup, handleSettings, path.startsWith, handleAdmin, notFound, faceForPath, headers.append, issueCookie, faceCookie, res.headers.append, console.error, redact
  • L247 · scheduled handler exported · references DB · calls tick.getUTCHours, tick.getUTCMinutes, snapshotGoalDays, console.log, deleteStaleSessions, deleteExpiredWebSessions, pruneRateLimits
  • L283 · redact calls (conditional paths may differ): String, replace, text.replace
  • L289 · robotsTxt calls (conditional paths may differ): join
  • L318 · tooManyRequests calls (conditional paths may differ): String
  • L337 · toLogin calls (conditional paths may differ): next.startsWith, encodeURIComponent

Environment references: env.DB

src/types.ts ↗
  • L196 · todayGoalLimit calls (conditional paths may differ): Number.isInteger
src/auth.ts ↗
  • L56 · timingSafeEqual calls (conditional paths may differ): Math.max, a.charCodeAt, b.charCodeAt
  • L67 · sha256Hex calls (conditional paths may differ): crypto.subtle.digest, encode, join, map, padStart, b.toString
  • L85 · userFromToken calls (conditional paths may differ): sha256Hex, findUserByTokenHash, timingSafeEqual
  • L104 · issueCookie calls (conditional paths may differ): randomHex, Date.now, createWebSession
  • L124 · revokeCookie calls (conditional paths may differ): SESSION_ID_RE.test, deleteWebSession, clearCookie
  • L134 · readCookie calls (conditional paths may differ): request.headers.get, header.split, part.indexOf, trim, part.slice
  • L145 · sessionIdFrom calls (conditional paths may differ): readCookie, SESSION_ID_RE.test
  • L161 · authenticate calls (conditional paths may differ): searchParams.get, userFromToken, sessionIdFrom, findUserByWebSession, Date.now

Environment references: env.DB

src/gate.ts ↗
  • L21 · json calls (conditional paths may differ): JSON.stringify
  • L51 · newSid calls (conditional paths may differ): crypto.getRandomValues, join, map, padStart, b.toString
  • L80 · handleGate calls (conditional paths may differ): url.searchParams.get, text, json, pass, refuse, userFromToken, getUserApp, passing, Date.now, getGraceUntil, insertEvent, newSid, createSession, toString, block
  • L140 · handleResolve calls (conditional paths may differ): request.text, form.get, json, getSession, Date.now, getUserApp, resolveSessionAtomically

Environment references: env.DB

src/ui/breathe.ts ↗
  • L41 · farewellLines calls (conditional paths may differ): t
  • L73 · renderBreathe calls (conditional paths may differ): themeFromParam, url.searchParams.get, localeOf, expiredPage, getSession, Date.now, Promise.all, getUserApp, getUserById, breathePage, clampWait, safeScheme, pickFarewell, translator
  • L116 · breathePage calls (conditional paths may differ): translator, page, breatheBody
  • L133 · expiredPage calls (conditional paths may differ): translator, page, t
  • L149 · breatheBody calls (conditional paths may differ): t, escapeHtml, orbHtml, jsonScript
  • L197 · clampWait calls (conditional paths may differ): Number.isFinite, Math.max, Math.min, Math.round
  • L212 · pickFarewell calls (conditional paths may differ): farewellLines, sid.charCodeAt

Environment references: env.DB

src/ui/mock.ts ↗
  • L18 · renderMock calls (conditional paths may differ): localeOf, translator, themeFromParam, url.searchParams.get, parseWait, parseLabel, breathePage, t, switcher
  • L43 · parseWait calls (conditional paths may differ): Number, Number.isFinite, Math.max, Math.min, Math.round
  • L49 · parseLabel calls (conditional paths may differ): trim, t
  • L57 · switcher calls (conditional paths may differ): themeParam, String, q.toString, themeTitle, t, link, again.toString
src/ui/review.ts ↗
  • L33 · renderReview calls (conditional paths may differ): localeOf, translator, getReviewStats, page, t, consoleHeader, emptyState, sections
  • L56 · sections calls (conditional paths may differ): join, todaySection, weekSection, appsSection, monthSection, footnote
  • L61 · todaySection calls (conditional paths may differ): card, t, escapeHtml, monthDay, splitBar
  • L84 · weekSection calls (conditional paths may differ): Math.max, s.week.map, join, dayColumn, t, pct, card
  • L95 · dayColumn calls (conditional paths may differ): t, shortWeekday, Math.max, Math.round, seg, escapeHtml
  • L118 · appsSection calls (conditional paths may differ): card, t, Math.max, s.apps.map, join, appRow
  • L130 · appRow calls (conditional paths may differ): escapeHtml, t, Math.max, Math.round, splitBar, pct
  • L145 · monthSection calls (conditional paths may differ): t, card, tile, String, pct
  • L163 · footnote calls (conditional paths may differ): parts.push, t, escapeHtml, parts.join
  • L175 · emptyState calls (conditional paths may differ): card, t
  • L185 · card calls (conditional paths may differ): escapeHtml
  • L192 · tile calls (conditional paths may differ): escapeHtml
  • L197 · splitBar calls (conditional paths may differ): seg
  • L208 · pct calls (conditional paths may differ): Math.round

Environment references: env.DB

src/ui/setup.ts ↗
  • L31 · renderSetup calls (conditional paths may differ): localeOf, translator, url.searchParams.get, revealToken, listUserApps, apps.find, t, icon, join, apps.map, escapeHtml, copyLine, lineFor, rawLineFor, recordSetupOpened, page, copyLinesScript, testScript, consoleHeader, inAppBrowserOf
  • L346 · fillBody calls (conditional paths may differ): sentence.replace
  • L380 · testScript calls (conditional paths may differ): fillBody.toString, jsonForScript, t
  • L433 · shortcutDiagram calls (conditional paths may differ): t, icon, hl, fold

Environment references: env.DB

src/ui/today.ts ↗
  • L32 · handleToday calls (conditional paths may differ): localeOf, render, translator, handlePost
  • L45 · field calls (conditional paths may differ): form.get, v.trim
  • L49 · intId calls (conditional paths may differ): test, Number
  • L70 · receipt calls (conditional paths may differ): Promise.all, listGoals, listCheckins, checkins.map, shownGoals, todayGoalLimit, done.has, shown.every, JSON.stringify
  • L93 · handlePost calls (conditional paths may differ): request.formData, bad, field, Date.now, request.headers.get, createGoal, back, intId, shanghaiDate, Promise.all, listGoals, listTasks, goals.some, notFound, tasks.some, toggleCheckin, receipt, setGoalTaskCheckins, syncGoalCheckin, getTask
  • L170 · render calls (conditional paths may differ): Date.now, shanghaiDate, Array.from, addDays, Promise.all, listGoals, listTasks, listCheckins, listTaskCheckins, liveGoals, todayGoalLimit, shownGoals, live.slice, checkins.map, taskCheckins.map, top.map, checked.has, map, tasks.filter, doneTasks.has
  • L228 · finHtml calls (conditional paths may differ): t
  • L232 · emptyState calls (conditional paths may differ): t
  • L242 · cardHtml calls (conditional paths may differ): escapeHtml, t, tasksHtml, join, c.dots.map, footHtml
  • L264 · tasksHtml calls (conditional paths may differ): join, c.tasks.map, taskRow
  • L269 · taskRow calls (conditional paths may differ): escapeHtml, t, chipHtml
  • L290 · chipHtml calls (conditional paths may differ): safeScheme, jumpHtml, jumpLabel
  • L305 · jumpLabel calls (conditional paths may differ): t, escapeHtml, test
  • L316 · jumpHtml calls (conditional paths may differ): test, escapeHtml, icon
  • L323 · bindHtml calls (conditional paths may differ): icon, t
  • L327 · goHtml calls (conditional paths may differ): safeScheme, bindHtml, jumpHtml, jumpLabel
  • L340 · footHtml calls (conditional paths may differ): goHtml, safeScheme, c.tasks.some, bindHtml
  • L346 · restFold calls (conditional paths may differ): icon, t, join, goals.map, escapeHtml
  • L353 · banner calls (conditional paths may differ): t

Environment references: env.DB

src/ui/goals.ts ↗
  • L27 · handleGoals calls (conditional paths may differ): localeOf, translator, render, queryId, handlePost
  • L47 · field calls (conditional paths may differ): form.get, v.trim
  • L52 · intId calls (conditional paths may differ): test, Number
  • L62 · queryId calls (conditional paths may differ): url.searchParams.get, intId
  • L67 · validDate calls (conditional paths may differ): DATE_RE.test, map, s.split, Date.UTC, t.getUTCFullYear, t.getUTCMonth, t.getUTCDate
  • L75 · validateTarget calls (conditional paths may differ): t, safeScheme
  • L83 · validate calls (conditional paths may differ): t, validateTarget, validDate
  • L97 · handlePost calls (conditional paths may differ): request.formData, render, t, field, Date.now, shanghaiDate, validate, intId, createGoal, seeOther, updateGoal, notFound, createTask, syncGoalCheckin, validateTarget, getTask, updateTaskTarget, deleteTask, setUserTodayGoals, goalOps.has
  • L236 · render calls (conditional paths may differ): shanghaiDate, Date.now, listGoals, listTasks, map, listTaskCheckins, byGoal.get, arr.push, byGoal.set, tasks.find, goals.filter, isExpired, escapeHtml, consoleHeader, t, todayGoalLimit, expiredBlock, addBlock, icon, join
  • L290 · expiredBlock calls (conditional paths may differ): join, goals.map, t, escapeHtml
  • L311 · addBlock calls (conditional paths may differ): icon, t, goalFields
  • L332 · goalFields calls (conditional paths may differ): id, t, escapeHtml, icon, schemeField
  • L361 · goalRow calls (conditional paths may differ): tasks.filter, doneToday.has, escapeHtml, t, icon, goalFields, join, tasks.map, taskRow
  • L417 · taskRow calls (conditional paths may differ): escapeHtml, t, icon, schemeField, fieldId
  • L460 · limitBlock calls (conditional paths may differ): options.push, t, options.join
  • L505 · archivedBlock calls (conditional paths may differ): icon, t, join, goals.map, escapeHtml, jsSingleQuotedBody

Environment references: env.DB

src/ui/todaysetup.ts ↗
  • L24 · renderTodaySetup calls (conditional paths may differ): localeOf, translator, consoleHeader, t, copyLine, page, copyLinesScript
src/ui/progress.ts ↗
  • L44 · renderProgress calls (conditional paths may differ): localeOf, translator, Date.now, shanghaiDate, addDays, mondayOf, Promise.all, listGoals, listGoalDays, listCheckins, countTaskCheckinsBetween, goals.filter, page, t, consoleHeader, emptyState, shownGoals, todayGoalLimit, top.map, map
  • L136 · monthStrip calls (conditional paths may differ): snapshotRows.map, byDate.set, Array.from, addDays, join, days.map, byDate.get, toFixed, Math.min
  • L166 · goalRowHtml calls (conditional paths may differ): join, dotDays.map, checkedDates.has, Math.min, daysSinceInclusive, escapeHtml, t
  • L173 · daysSinceInclusive calls (conditional paths may differ): shanghaiDate, map, created.split, today.split, Math.round, Date.UTC
  • L181 · mondayOf calls (conditional paths may differ): map, date.split, getUTCDay, Date.UTC, addDays
  • L187 · emptyState calls (conditional paths may differ): t

Environment references: env.DB

src/ui/surf.ts ↗
  • L92 · handleSurf calls (conditional paths may differ): localeOf, render, translator, handlePost
  • L113 · farewellLines calls (conditional paths may differ): t
  • L133 · groundingLines calls (conditional paths may differ): t
  • L145 · field calls (conditional paths may differ): form.get, v.trim
  • L149 · intId calls (conditional paths may differ): test, Number
  • L161 · ok calls (conditional paths may differ): JSON.stringify
  • L168 · handlePost calls (conditional paths may differ): request.formData, bad, field, Date.now, request.headers.get, findOpenUrge, ok, back, sceneTrigger, createUrge, intId, bumpUrgeRound, notFound, finishUrge, getUrge
  • L230 · render calls (conditional paths may differ): Date.now, shanghaiDate, Number, today.slice, listUrgesSince, summarizeUrges, request.headers.get, test, inAppBrowserOf, sceneOf, t, groundingLines, farewellLines, step0, step1, step2, step3, banner, jsonScript, page
  • L312 · step0 calls (conditional paths may differ): sceneOf, user.surf_line.trim, t, escapeHtml, hasScene
  • L324 · cells calls (conditional paths may differ): repeat
  • L344 · step1 calls (conditional paths may differ): t, cells, groundingLines, orbHtml
  • L378 · step2 calls (conditional paths may differ): t
  • L406 · step3 calls (conditional paths may differ): join, days.map, Math.min, t
  • L425 · banner calls (conditional paths may differ): t

Environment references: env.DB

src/ui/surfreview.ts ↗
  • L50 · renderSurfReview calls (conditional paths may differ): localeOf, translator, Date.now, shanghaiDate, listUrgesSince, summarizeUrges, page, t, consoleHeader, entryBlock, emptyState, dayDots, escapeHtml, monthDay, hourBars
  • L103 · entryBlock calls (conditional paths may differ): t
  • L108 · emptyState calls (conditional paths may differ): t
  • L113 · dayDots calls (conditional paths may differ): join, days.map, Math.min
  • L127 · hourBars calls (conditional paths may differ): Math.max, join, hours.map, toFixed

Environment references: env.DB

src/ui/surfsetup.ts ↗
  • L36 · handleSurfSetup calls (conditional paths may differ): localeOf, translator, renderSurfSetup, handlePost
  • L50 · renderSurfSetup calls (conditional paths may differ): hasScene, sceneOf, join, SCENE_KEYS.map, t, consoleHeader, escapeHtml, page
  • L110 · handlePost calls (conditional paths may differ): request.formData, bad, field, isSceneKey, setUserSurfScene, storedScene, seeOther
  • L129 · field calls (conditional paths may differ): form.get, v.trim

Environment references: env.DB

src/ui/pwa.ts ↗
  • L83 · icon calls (conditional paths may differ): atob, bin.charCodeAt
  • L92 · iconResponse calls (conditional paths may differ): icon
  • L100 · surfIcon calls (conditional paths may differ): atob, bin.charCodeAt
  • L109 · surfIconResponse calls (conditional paths may differ): surfIcon
src/api/candidates.ts ↗
  • L76 · toOut calls (conditional paths may differ): isCorroborated, c.sources.map
  • L110 · searchAppStore calls (conditional paths may differ): setTimeout, controller.abort, encodeURIComponent, f, res.json, Array.isArray, apps.push, clearTimeout
  • L159 · searchCandidates calls (conditional paths may differ): q.trim, findApps, table.map, a.candidates.map, searchAppStore, deriveFromBundleId, hits.push, suggestKey, candidates.map
  • L217 · localiseCopy calls (conditional paths may differ): out.hits.map, h.candidates.map, t
  • L232 · handleCandidates calls (conditional paths may differ): searchParams.get, searchCandidates, q.slice, JSON.stringify, localiseCopy, translator, localeOf
src/ui/console.ts ↗
  • L106 · cookieValue calls (conditional paths may differ): request.headers.get, header.split, part.indexOf, trim, part.slice
  • L118 · faceFromRequest calls (conditional paths may differ): cookieValue
  • L135 · faceForPath calls (conditional paths may differ): path.startsWith
  • L215 · consoleHeader calls (conditional paths may differ): faceOf, icon, map, FACES.filter, t, escapeHtml, join, otherFaces.map, faceTabs.map, tab
src/ui/account.ts ↗
  • L71 · handleRegister calls (conditional paths may differ): localeOf, translator, turnstileKeys, registerPage, methodNotAllowed, readForm, t, field, secret, verifyTurnstile, register, encodeURIComponent, accountErrorMessage, seeOther
  • L157 · registerPage calls (conditional paths may differ): gatePage, t, banner, emailField, escapeHtml, passwordField, turnstileWidget
  • L209 · handleLogin calls (conditional paths may differ): localeOf, translator, safeNext, q.get, loginPage, methodNotAllowed, readForm, t, field, login, secret, accountErrorMessage, seeOther
  • L254 · loginPage calls (conditional paths may differ): gatePage, t, banner, encodeURIComponent, emailField, passwordField
  • L291 · handleClaim calls (conditional paths may differ): localeOf, translator, claimPage, methodNotAllowed, readForm, t, field, secret, claimAccount, accountErrorMessage, seeOther
  • L325 · claimPage calls (conditional paths may differ): gatePage, t, banner, tokenField, emailField, escapeHtml, passwordField
  • L361 · handleRecover calls (conditional paths may differ): localeOf, translator, recoverPage, methodNotAllowed, readForm, t, secret, resetPasswordWithToken, field, accountErrorMessage, seeOther
  • L387 · recoverPage calls (conditional paths may differ): gatePage, t, banner, tokenField, passwordField
  • L416 · handleAccount calls (conditional paths may differ): localeOf, translator, faceFromRequest, accountPage, q.get, q.has, methodNotAllowed, readForm, t, field, seeOther, logout, sessionIdFrom, rotateToken, secret, accountErrorMessage, changePassword, passwordChangeMessage
  • L488 · passwordChangeMessage calls (conditional paths may differ): t, accountErrorMessage
  • L504 · rotateCard calls (conditional paths may differ): t
  • L519 · rotatedCard calls (conditional paths may differ): t, escapeHtml
  • L540 · accountPage calls (conditional paths may differ): accountSummary, revealToken, page, t, consoleHeader, banner, welcomeBanner, escapeHtml, shanghaiDate, rotatedCard, tokenCard, passwordCard, bindCard, rotateCard, languageCard, copyScript
  • L600 · languageCard calls (conditional paths may differ): t, link
  • L612 · welcomeBanner calls (conditional paths may differ): banner, t
  • L640 · tokenCard calls (conditional paths may differ): t, escapeHtml
  • L677 · passwordCard calls (conditional paths may differ): t, passwordField
  • L692 · bindCard calls (conditional paths may differ): t
  • L716 · copyScript calls (conditional paths may differ): jsonForScript, t
  • L763 · gatePage calls (conditional paths may differ): page, langSwitch
  • L793 · safeNext calls (conditional paths may differ): sameSitePath
  • L828 · sameSitePath calls (conditional paths may differ): path.startsWith
  • L851 · banner calls (conditional paths may differ): escapeHtml
  • L863 · emailField calls (conditional paths may differ): t, escapeHtml
  • L886 · tokenField calls (conditional paths may differ): t
  • L913 · turnstileWidget calls (conditional paths may differ): escapeHtml, t
  • L924 · readForm calls (conditional paths may differ): request.formData
  • L932 · field calls (conditional paths may differ): form.get, v.trim
  • L938 · secret calls (conditional paths may differ): form.get
  • L943 · seeOther calls (conditional paths may differ): headers.append
src/ui/settings.ts ↗
  • L26 · handleSettings calls (conditional paths may differ): localeOf, translator, renderSettings, url.searchParams.get, handlePost
  • L44 · handlePost calls (conditional paths may differ): request.formData, renderSettings, t, field, validAppKey, deleteUserApp, seeOther, getUserApp, form.has, validate, upsertUserApp, encodeURIComponent
  • L154 · validAppKey calls (conditional paths may differ): APP_KEY.test
  • L159 · validate calls (conditional paths may differ): validAppKey, t, SCHEME_PREFIX.test, d.scheme.toLowerCase, some, forbiddenPrefixes, lower.startsWith, int
  • L194 · field calls (conditional paths may differ): form.get, v.trim
  • L199 · int calls (conditional paths may differ): test, Number
  • L224 · renderSettings calls (conditional paths may differ): listUserApps, apps.some, join, apps.map, appRow, fromDraft, consoleHeader, t, inAppNotice, inAppBrowserOf, escapeHtml, addBlock, emptyState, page, schemeFieldJs
  • L279 · fromDraft calls (conditional paths may differ): int
  • L304 · inAppNotice calls (conditional paths may differ): escapeHtml, icon, t
  • L313 · emptyState calls (conditional paths may differ): t, icon
  • L333 · addBlock calls (conditional paths may differ): icon, t, escapeHtml, labelField, schemeField, secondsFields, int, enabledField
  • L381 · appRow calls (conditional paths may differ): escapeHtml, icon, t, labelField, schemeField, secondsFields, enabledField, jsSingleQuotedBody
  • L416 · labelField calls (conditional paths may differ): fieldId, t, escapeHtml
  • L435 · secondsFields calls (conditional paths may differ): fieldId, icon, t, hl, fold
  • L457 · enabledField calls (conditional paths may differ): fieldId, t

Environment references: env.DB

src/api/admin.ts ↗
  • L66 · handleAdmin calls (conditional paths may differ): forbidden, faceFromRequest, renderAdmin, localeOf, handleCreateUser
  • L85 · forbidden calls (conditional paths may differ): page
  • L104 · handleCreateUser calls (conditional paths may differ): faceFromRequest, request.formData, renderAdmin, form.get, raw.trim, randomHex, sealToken, createUser, sha256Hex
  • L163 · renderAdmin calls (conditional paths may differ): translator, onboardingCounts, shanghaiDate, Date.now, Promise.all, listUsers, countAttemptsPerUser, attemptsById.set, users.map, attemptsById.get, consoleHeader, escapeHtml, oneTimePanel, onboardingPanel, join, rows.map, privacyNote, page
  • L214 · oneTimePanel calls (conditional paths may differ): escapeHtml
  • L225 · userRow calls (conditional paths may differ): escapeHtml

Environment references: env.TOKEN_KEY · env.DB

src/ui/guides.ts ↗
  • L6 · renderGuide calls (conditional paths may differ): localeOf, translator, t, page, escapeHtml, langSwitch
  • L52 · sitemap calls (conditional paths may differ): join, PUBLIC_PATHS.map, escapeHtml
src/ui/landing.ts ↗
  • L16 · renderLanding calls (conditional paths may differ): localeOf, translator, langSwitch, page, t
src/db.ts ↗
  • L24 · shanghaiDate calls (conditional paths may differ): DAY_FORMATTER.format
  • L39 · findUserByTokenHash calls (conditional paths may differ): first, bind, db.prepare
  • L48 · getUserById calls (conditional paths may differ): first, bind, db.prepare
  • L55 · listUsers calls (conditional paths may differ): all, db.prepare
  • L65 · setUserLocale calls (conditional paths may differ): run, bind, db.prepare
  • L72 · setUserTodayGoals calls (conditional paths may differ): run, bind, db.prepare
  • L82 · createUser calls (conditional paths may differ): run, bind, db.prepare, Date.now, Number
  • L131 · findAccountByEmail calls (conditional paths may differ): first, bind, db.prepare
  • L138 · findAccountById calls (conditional paths may differ): first, bind, db.prepare
  • L154 · createAccount calls (conditional paths may differ): run, bind, db.prepare, Date.now, Number
  • L201 · attachAccount calls (conditional paths may differ): run, bind, db.prepare
  • L234 · updateUserPassword calls (conditional paths may differ): db.batch, bind, db.prepare
  • L244 · getSealedToken calls (conditional paths may differ): first, bind, db.prepare
  • L261 · isEmailTakenError calls (conditional paths may differ): String, parts.push, parts.some, test
  • L269 · createWebSession calls (conditional paths may differ): run, bind, db.prepare
  • L279 · getWebSession calls (conditional paths may differ): first, bind, db.prepare
  • L296 · findUserByWebSession calls (conditional paths may differ): first, bind, db.prepare
  • L307 · deleteWebSession calls (conditional paths may differ): run, bind, db.prepare
  • L316 · deleteWebSessionsForUser calls (conditional paths may differ): run, bind, db.prepare
  • L326 · deleteExpiredWebSessions calls (conditional paths may differ): run, bind, db.prepare
  • L333 · getUserApp calls (conditional paths may differ): first, bind, db.prepare
  • L343 · listUserApps calls (conditional paths may differ): all, bind, db.prepare
  • L354 · upsertUserApp calls (conditional paths may differ): run, bind, db.prepare
  • L368 · deleteUserApp calls (conditional paths may differ): run, bind, db.prepare
  • L385 · rotateUserToken calls (conditional paths may differ): db.batch, bind, db.prepare
  • L403 · createSession calls (conditional paths may differ): run, bind, db.prepare
  • L413 · getSession calls (conditional paths may differ): first, bind, db.prepare
  • L426 · deleteStaleSessions calls (conditional paths may differ): run, bind, db.prepare
  • L434 · insertEvent calls (conditional paths may differ): run, bind, db.prepare, shanghaiDate
  • L445 · countEventsByKind calls (conditional paths may differ): all, bind, db.prepare
  • L465 · countEventsByDay calls (conditional paths may differ): all, bind, db.prepare
  • L499 · countAttemptsPerUser calls (conditional paths may differ): all, bind, db.prepare
  • L535 · resolveSessionAtomically calls (conditional paths may differ): bind, db.prepare, shanghaiDate, statements.push, db.batch
  • L582 · getGraceUntil calls (conditional paths may differ): first, bind, db.prepare
  • L590 · setGrace calls (conditional paths may differ): run, bind, db.prepare
  • L605 · listGoals calls (conditional paths may differ): all, bind, db.prepare
  • L617 · getGoal calls (conditional paths may differ): first, bind, db.prepare
  • L627 · createGoal calls (conditional paths may differ): run, bind, db.prepare, Number
  • L643 · updateGoal calls (conditional paths may differ): run, bind, db.prepare
  • L659 · setGoalArchived calls (conditional paths may differ): run, bind, db.prepare
Build and deployment pipeline · 0 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.

No GitHub Actions workflow was found in the collected tree. Deployment may be manual or configured elsewhere.

package.json ↗
  • deploy: wrangler d1 migrations apply yixi --remote && wrangler deploy
  • migrate:local: wrangler d1 migrations apply yixi --local
  • migrate:remote: wrangler d1 migrations apply yixi --remote

Full upstream document by @Defiabell · README.md · snapshot 2b679f1

English | 简体中文

yixi (一息)

一息 does two things, sharing one account. Breathe (拦): before a distracting app opens, your phone shows a page that asks you to breathe for ten seconds, then offers 「算了」 first and 「继续打开」 second, and keeps the receipt. Today (引): your top few goals for the coming weeks — three by default, as many as nine if you want — on one page you open every morning, each with its sub-tasks for today, seven dots for the last seven days, and a button that jumps straight into the app where the work happens — B 站 for a workout, 微信读书 for a book. Either half is useful on its own.

One Cloudflare Worker and one D1 database, running entirely on Cloudflare's free tier. On the interception side it is a self-hosted stand-in for One Sec, built as a web page instead of an iPhone app.

The breathing page in light mode: an ink blot on paper, 「算了」 as a filled pill, 「继续打开」 as a small underlined link

「算了」 is a filled pill, 「继续打开」 a small underlined link — the asymmetry is deliberate.

Before a distracting app opens, the phone jumps to a page that counts ten seconds out, offers the way out first and the way in second, and keeps the receipt either way.

Breathe →

/today in light mode: three goal cards on paper, the first a large card carrying the goal and today's sub-tasks, each with its own check circle and jump chip, plus seven dots; a card with no sub-tasks shows a wide jump button instead

Three goals, the first one large. No streak number anywhere.

The few things that matter for the coming weeks on one page you open every morning — each with its sub-tasks for today, seven dots for the last seven days, and one button into the app where the work happens.

Today →

Try it now →
An open instance, free, nothing to deploy. Or run your own in fifteen minutes.

Who this is for: anyone who wants a nudge rather than a wall. It is friction, not enforcement — the automation is one toggle away from off, on purpose. A third face, Surf (渡): when an urge shows up, tap it, and it walks you through ten minutes.

The open instance is the quickest way in. If you would rather not keep a minute-by-minute log of your worst impulses on someone else's server, deploy your own in fifteen minutes; it is the same code either way.

Known limits

Three that apply to the whole product. The per-face lists are in docs/breathe.md and docs/today.md, and both are worth reading before you deploy.

  • The UI is bilingual (Chinese and English). Switch it from the footer on the landing, login and register pages, from the language section on /account, or by adding ?lang=en / ?lang=zh to any page; Chinese is the default whenever nothing else says otherwise. Every page is translated, the long Shortcut walkthrough at /setup included; the iOS labels it quotes are the ones English iOS prints.
  • From mainland China, use the Pages hostname. See Why it deploys twice. pages.dev is a shared suffix and clean today is not clean forever — your own domain is the only durable answer.
  • This is a nudge, not a blocker. Anyone can disable the automation in two taps. That is by design — the whole system fails open — and it means the tool only works for someone who wants it to.

Pages

Route Who What
/ anyone landing page, links to register / sign in
/gate?app=&k= gate token the decision endpoint the Shortcut calls; answers pass or a URL
/b?s=<sid> sid the breathing page
POST /resolve sid records proceed / abandon, opens the grace window
/register /login /claim /recover anyone sign up, sign in, bind an old token, reset a password with a token
/today you the one page to open every morning: your top goals, today's sub-tasks, a seven-day dot strip
/today/goals you add, edit, reorder and archive goals — everything /today shows but does not let you change
/today/review you looking back: today's ratio, a 30-day strip, every goal's own dot strip and check-in rate, this week's sub-task check-ins
/today/setup you add /today to the home screen, a Shortcut, or a timed automation that opens it on its own
/goals you kept as a 307 to /today/goals (preserves method/body), so old links and bookmarks still land somewhere useful
/review you today, the last seven days, which app costs you most
/settings you which apps to intercept, and how long
GET /api/candidates you JSON: type an app name, get candidate URL schemes with sources. Fetched by the URL scheme field on /settings and /today/goals; not a page
/lookup /probe — kept as 302s to /settings. Both were pages once; finding and testing a scheme is now part of the field that needs it, so old links and bookmarks still land somewhere useful
/setup you the Shortcut walkthrough, with your own host and token filled in
/surf you the Surf (渡) ten-minute flow: put the phone down, let it pass, then decide again. Nothing on it asks you anything
/surf/review you Surf's 30-day look-back: urge counts, the passed/opened ratio, and what time of day they come. Also the entry point for the 渡 face
/surf/setup you pick your scene, once
/account you read your gate token back, change your password, sign out
/mock?v=1|2 anyone the two candidate visual skins, side by side
/admin owner mint a token for someone offline; see per-user attempt counts
/manifest.webmanifest /icon.png anyone the home-screen files — public and cacheable, nothing per-user in either
/robots.txt anyone crawl the front door, nothing else

Anything else is a 404. There is no detail endpoint under /admin to guess at — see SECURITY.md.

Each face carries its own nav — 「今日」 for /today and behind it, 「拦截」 for the breathing pages and behind them, 「渡」 for /surf and behind it — with a small link to each of the others, and all three share the same login.

Why it deploys twice

One codebase, two Cloudflare deployments, sharing one D1 database:

Deployment Config Job
Pages pages/wrangler.toml the hostname people actually open
Worker wrangler.toml one daily trigger — cleanup at noon Shanghai, the goal_days snapshot at 00:00

This is worth reading even if you are nowhere near China, because it is a real and reusable piece of operational knowledge about Cloudflare's shared hostnames.

*.workers.dev is DNS-poisoned inside mainland China. Measured, not assumed: a *.workers.dev hostname resolves to three mutually different addresses from the three big domestic public resolvers (223.5.5.5, 119.29.29.29, 114.114.114.114), none of them matching what the rest of the world sees. That is the signature of domain-level interference, not of Cloudflare being blocked — cloudflare.com and *.pages.dev resolve byte-for-byte identically inside and outside. The shared workers.dev suffix is being singled out.

*.pages.dev is currently clean, so Pages gets the human-facing hostname. Same edge, same runtime, same code, same database; only the hostname differs. pages/functions/[[path]].ts is one line that forwards every request into the same Worker fetch handler.

The Worker deployment stays because Pages has no Cron Triggers. Both daily ticks are fired by Cloudflare itself, so neither cares that its own hostname is unreachable from China.

Two things to know before copying this pattern:

  • Cloudflare's own tooling recommends Workers over Pages for new projects. Deploying to Pages here is going against that advice, purely to get a usable hostname.
  • If you have your own domain, do that instead. Attach it to the Worker as a Custom Domain and both problems disappear at once — you get a clean hostname and Cron Triggers, and the entire pages/ directory can be deleted. pages.dev is a shared suffix too; clean today is not a guarantee.

Also worth knowing: wrangler pages deploy does not accept a -c config path, which is why the Pages config has to live in its own directory rather than sharing the Worker's wrangler.toml.

When updating an existing install, deploy the Worker and Pages in the same sitting — in between, the Pages hostname keeps serving the old build, and a sub-task check made there is written to the retired done_at column and never shown by the new code.

Deploy your own (~15 min)

Prerequisites: a Cloudflare account and Node 18+.

1. Install and sign in

cd yixi
npm install
npx wrangler login

2. Create the D1 database

npx wrangler d1 create yixi

Put the returned database_id into both wrangler.toml and pages/wrangler.toml. Both files must point at the same database — that is what makes the two deployments one app. (The database_name must stay yixi, or change it in both files and in package.json's scripts.)

3. Generate the secrets — and keep a copy

openssl rand -base64 48    # this is your TOKEN_KEY
openssl rand -base64 48    # this is your COOKIE_SECRET

Do not pipe these straight into wrangler secret put. You need to type the same TOKEN_KEY into two deployments, and there is no way to read a Cloudflare secret back out.

⚠️ Lose TOKEN_KEY and nobody can ever read their gate token again.

TOKEN_KEY is the AES-GCM key that the gate tokens are sealed under so a signed-in person can look their own token up. It is never written to D1 — a stolen database on its own opens nothing.

Interception keeps working without it: /gate verifies against a SHA-256 hash and never touches the ciphertext. What breaks is recovery. Anyone who forgot their token can no longer read it back, which also means they can no longer set up a new phone, and /recover (reset a password using the token) becomes unusable for them. There is no reset path and no way to re-derive it. Save it in a password manager before you continue.

Then set them on the Worker:

npx wrangler secret put TOKEN_KEY        # paste the first value
npx wrangler secret put COOKIE_SECRET    # paste the second value

COOKIE_SECRET is legacy. Browser sessions used to be a signed cookie; they are now rows in sessions_web and the cookie carries only an opaque id, so nothing signs anything any more. No code reads it. It is documented here because existing deployments still have it set and because dropping a secret is a nuisance — a fresh deployment can leave it out.

4. Apply the schema and deploy the Worker

npm run deploy    # applies migrations against the remote D1, then deploys

Migrations only need to run once; the Pages deployment shares the same database.

Always deploy with npm run deploy, never a bare wrangler deploy — it is the migrations-then-deploy order that keeps the two in step. The current Worker needs 0006_user_locale.sql, 0008_today_goals.sql and 0011_surf_scene.sql in particular: /gate reads users.locale, users.today_goals, users.surf_scene and users.surf_line on its way to knowing who you are, so a Worker deployed against a database without those columns stops intercepting anything.

5. Deploy Pages

cd pages
npx wrangler pages project create yixi-app --production-branch main

# TOKEN_KEY must be byte-for-byte the SAME value as the Worker's, or Pages
# cannot open tokens that were sealed on the Worker side (and vice versa).
npx wrangler pages secret put TOKEN_KEY --project-name yixi-app
npx wrangler pages secret put COOKIE_SECRET --project-name yixi-app

npx wrangler pages deploy --branch main --project-name yixi-app

You get a https://<project>.pages.dev. Note that *.pages.dev subdomains are globally unique — if the name is taken, Cloudflare appends a suffix, and that suffixed hostname is the one to use everywhere below.

6. Turn on the bot check (optional, and skipping it is supported)

Registration is open to anyone who finds the URL, and since this repository is public, the URL is too. The per-IP throttle in src/ratelimit.ts holds one address to 5 sign-ups an hour; it does nothing about a script spread over a few hundred addresses. Cloudflare Turnstile closes that gap, on /register and nowhere else.

  1. Cloudflare dashboard → Turnstile → Add widget. Widget mode Managed.
  2. Under hostnames, add both deployments — <project>.pages.dev and <worker>.<subdomain>.workers.dev — because both of them serve /register. Add localhost too if you want the challenge to appear in npm run dev.
  3. Copy the two values it hands you: a site key (public — it is rendered into the page) and a secret key (never leaves the server).
  4. Set both on both deployments:
# Worker
npx wrangler secret put TURNSTILE_SITE_KEY   # paste the site key
npx wrangler secret put TURNSTILE_SECRET     # paste the secret key

# Pages — the same two values
cd pages
npx wrangler pages secret put TURNSTILE_SITE_KEY --project-name yixi-app
npx wrangler pages secret put TURNSTILE_SECRET --project-name yixi-app

TURNSTILE_SITE_KEY is public, so it could equally be a [vars] entry in wrangler.toml. It is a secret here only so the two values travel together and cannot get half-deployed.

Leaving this out is a supported configuration, not a broken one. With either value missing, no widget renders, nothing is verified, and /register behaves exactly as it did before this existed — which is what lets npm run dev and a first deploy work with no Cloudflare widget at all. The price is worth saying out loud: no keys means no bot protection beyond the per-IP throttle. Same fail-open judgment as everywhere else in this product — see SECURITY.md.

Two more things worth knowing:

  • This is the only place the "zero external requests" rule is broken. The widget script loads from challenges.cloudflare.com, which is the single origin the CSP allows — on /register, and only while the keys are set. Every other page keeps default-src 'none' with no exceptions. See the comment above TURNSTILE_ORIGIN in src/ui/layout.ts.
  • The challenge needs JavaScript. With a widget configured, a browser with JavaScript off cannot sign up. The form says so.

7. Register, then make yourself owner

Open https://<your-host>/register and sign up with an email and a password. That is all a normal user ever needs; registration is open (behind the Turnstile challenge, if you set one up in step 6).

Owner is a separate thing, and there is deliberately no UI to grant it. If you want /admin (minting tokens for people offline), flip the flag directly:

npx wrangler d1 execute yixi --remote --command \
  "UPDATE users SET is_owner = 1 WHERE email = 'you@example.com';"

/admin is entirely optional. Since registration is open, it supports issuing tokens and viewing aggregate onboarding conversion. See measurement definitions.

8. Set up your first app

  1. /settings — add an app. The app key (e.g. xhs) is the string you will retype inside the iOS automation, and it must match exactly. Lowercase letters, digits, - and _ only.
  2. In the same form, the URL scheme field carries everything you need for it: a worked example above the box, a 试跳 button beside it, and a folded 「不知道填什么?按 App 名字找」 that lists candidates inline, each labelled with where it came from. None of them is verified.
  3. Do this on the iPhone. Tap 试跳 on a candidate — only the one that actually opens the app counts. Then 「用这个」 writes it into the box and you save. Your half-filled form survives the jump.
  4. /setup — the Shortcut walkthrough, with your real host and token already pasted into the lines you need.

Stack

Runtime Cloudflare Workers (also deployed as a Pages Function)
Storage Cloudflare D1 (SQLite)
Language TypeScript, strict, no runtime dependencies
Rendering server-side HTML, inline CSS/JS, zero external requests (CSP-enforced) — one exception: the Turnstile widget on /register, only when configured
Crypto WebCrypto only — PBKDF2-SHA256 passwords, AES-GCM token sealing
Client iOS Shortcuts + Safari
Tests 843 tests over 36 files (Vitest + @cloudflare/vitest-pool-workers)
Cost fits inside Cloudflare's free tier

Project layout

src/index.ts        route table, three auth shapes, the twice-daily cron
src/gate.ts         /gate and /resolve — the only machine-facing routes
src/auth.ts         ?k= token, cookie session, constant-time compares
src/account.ts      register / login / claim / recover; the closed recovery loop
src/crypto.ts       PBKDF2 passwords, AES-GCM token sealing, random hex
src/db.ts           every D1 statement in the app, and nothing else
src/stats.ts        /review aggregation; the grace_pass exclusion lives here
src/urges.ts        D1 helpers for the urges table, plus the pure aggregation /surf/review reads off them
src/surfscenes.ts   the five 渡 scenes: opening line and three body exits each, and how a stored scene resolves
src/snapshot.ts      the goal_days snapshot: what /today showed, what got done
src/ratelimit.ts    per-IP fixed-window throttle for the open endpoints
src/turnstile.ts    the optional /register challenge, and its fail-open rules
src/scheme.ts       the URL-scheme denylist — one authority, three call sites
src/schemes.ts      frozen snapshot of two public scheme collections (60 apps)
src/types.ts        Env, User, event kinds, the shared constants
src/dates.ts        'YYYY-MM-DD' arithmetic shared by /today and /today/goals
src/ui/*.ts         one module per page, all server-rendered
src/ui/schemefield.ts  the URL-scheme picker field shared by /settings and /today/goals
src/ui/breathing.ts the breathing orb shared by /b and /surf: markup, CSS and timing
src/ui/pwa.ts       the home-screen manifest and icon — public, no per-user data
src/ui/today.ts     /today — the morning page: goals, today's sub-tasks, seven-day dots
src/ui/goals.ts     /today/goals — add, edit, reorder and archive goals
src/ui/progress.ts  /today/review — looking back: the 30-day strip, per-goal check-in rate
src/ui/todaysetup.ts  /today/setup — home screen, Shortcut and timed-automation walkthrough
src/ui/surf.ts      /surf — the ten-minute urge-surfing flow, one document, four steps, no questions
src/ui/surfreview.ts  /surf/review — the 30-day look-back over recorded urges
src/ui/surfsetup.ts  /surf/setup — pick the scene once, plus the home-screen and Shortcut entry points
src/api/admin.ts    the owner's ticket window, and the privacy line
migrations/*.sql    D1 schema, eleven migrations
scripts/icon.mjs    regenerates the base64 PNG baked into src/ui/pwa.ts
pages/              Pages entry point (one line) + its own wrangler.toml
shortcut/README.md  why the Shortcut is shaped the way it is
docs/architecture.md  request lifecycle, tables, accounting semantics

Development

npm test               # vitest run — the full suite
npm run typecheck      # tsc --noEmit (src) + tsc -p test --noEmit
npm run dev            # wrangler dev — local server
npm run migrate:local  # apply migrations to the local D1
npm run deploy         # remote migrations, then deploy the Worker

Before changing anything, read CONTRIBUTING.md. It is short, and every rule in it comes from something that actually broke.

Docs

  • docs/breathe.md — the Shortcut, the grace window, the fail-open rule, interception limits
  • docs/today.md — the goal model, /today and the pages behind it, snapshots, the home screen
  • SECURITY.md — threat model, the token-storage trade-off, self-hosting caveats
  • CONTRIBUTING.md — the five constraints that must not be refactored away
  • docs/architecture.md — request lifecycle, D1 tables, accounting semantics
  • shortcut/README.md — the reasoning behind the Shortcut's shape (Chinese)

The author runs an open instance at https://yixi-app.pages.dev. Sign up there and you can be breathing before your apps in about ten minutes, with nothing to deploy.

What the operator can and cannot see. Your records are yours: the admin page returns a per-account count of how many times you were stopped, plus cohort-wide onboarding counts — no timestamps, no app names, no give-up rate, not even your email address. That is enforced in the SQL rather than in the template, and a test fails if it ever regresses. But be clear-eyed about the shape of the guarantee: whoever runs an instance holds its database, and a database can be queried directly. That is true of this instance and of every other self-hosted service you sign up for.

So: use the shared one if you want to try it without work, and run your own if you would rather that sentence not apply to you. Deploying takes about fifteen minutes and the instructions are above.

License

MIT

Public guides

The homepage links to /guides/iphone-shortcuts and /compare/one-sec. Both are server-rendered, bilingual, available without an account, and listed with / in /sitemap.xml. Language selection follows the existing cookie and Accept-Language behavior; these are not separate language URLs. The personal /setup guide remains authenticated and excluded from indexing. Comparison sources were checked on 2026-09-18.

Frequently asked about yixi

What is yixi?+

yixi is a self-hosted One Sec alternative built on the Cloudflare developer platform. A breathing pause before distracting apps, plus a small daily-goal page.

What does yixi replace?+

yixi is listed as an alternative to One Sec. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does yixi use?+

yixi is built on D1, Pages, Workers.

How much does yixi cost to run?+

The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply. Provide an iPhone with Shortcuts/Safari and configure the app automation; the website alone does not intercept app launches. Use the documented Pages+Worker setup with the same D1 and TOKEN_KEY in both, or an owned custom-domain Worker; replace both upstream database IDs. Keep dynamic requests below the account-wide Workers Free allowance and D1 row/storage use within limits; measure PBKDF2 login CPU against 10 ms. Registration is open and Turnstile is optional; the tool is a user-controlled nudge, not enforced blocking. The two cron ticks and persistent event history consume database usage; custom domain registration is optional/separate. Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured. Dynamic Pages Functions requests share the Workers account allowance; only requests that do not invoke Functions count as free unlimited static asset requests. D1 Free allowance: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; unindexed scans and history retention consume quota. Check current Cloudflare pricing before deploying.

Is yixi open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/Defiabell/yixi/2b679f164f447be88b2eff5bee0c2c04503d9814/LICENSE. Source code and contributor credit are available at https://github.com/Defiabell/yixi.

Discussion · 0

sign in to comment →
No comments yet — be the first.