Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 46 additions & 132 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,163 +6,57 @@ Private by default. Works offline. Open source.

---

## What it's for
Write a note. When it's about something you meant to do, it comes back later and asks one
question — *anything come of this?* — with four answers: **Not yet · Add update · Done · Let go.**

You write things down and then you forget them. Not because you're careless — because a note just
sits there. Nothing about it ever asks for your attention again.
No due dates, no streaks, no guilt.

DailyNote is a normal notes app: open it, write, close it. Nothing to set up, no account, no
internet needed. But when a note is about something you actually meant to do, it comes back to you
later and asks one question:
The ones worth showing people, you can publish to a **Feed** — note by note, afterwards, and only
if you want to. Nothing is public unless you say so, and writing needs no account at all.
Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use afterward in both documents.

  • README.md#L14-L15: replace afterwards with afterward.
  • docs/about.md#L42-L45: replace afterwards with afterward.
🧰 Tools
🪛 LanguageTool

[locale-violation] ~14-~14: In American English, ‘afterward’ is the preferred variant. ‘Afterwards’ is more commonly used in British English and other dialects.
Context: ...n publish to a Feed — note by note, afterwards, and only if you want to. Nothing is pu...

(AFTERWARDS_US)


[style] ~14-~14: This phrasing can be overused. Try elevating your writing with a more formal alternative.
Context: ...** — note by note, afterwards, and only if you want to. Nothing is public unless you say so, a...

(IF_YOU_WANT)

📍 Affects 2 files
  • README.md#L14-L15 (this comment)
  • docs/about.md#L42-L45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 14 - 15, Replace “afterwards” with “afterward” in
README.md lines 14-15 and docs/about.md lines 42-45, preserving the surrounding
text.

Source: Linters/SAST tools


> **Anything come of this?**

You get four answers. *Not yet.* *Add update.* *Done.* *Let go.*

That's the whole idea. Not reminders, not deadlines, not a streak you'll feel bad about breaking.
Just a note that shows up again at a sensible moment and gives you an easy way to say what
happened — including "nothing, and I'm letting it go."

There are no due dates and no overdue badges anywhere in this app. Feeling guilty is the thing we
are trying to avoid, not the thing we're trying to cause.

## And if you want, you can share

Some of what you write turns out to be worth showing people. A thing you said you'd learn, and
then did. A project that went somewhere.

So there's a **Feed**. You can publish a note you already wrote, or write a post straight to the
feed if that's what you feel like. You can read what other people have shared and follow the ones
worth following.

Two promises about it:

- **Your notes are private and stay that way.** Publishing is something you choose, note by note,
afterwards. Nothing you write is public unless you say so.
- **You never have to.** No account is needed to write notes. If you never publish anything, you
haven't missed out on anything — you just have a notes app, which is the point.

The writing screen never asks who's going to see it. Posting lives on its own tab, behind its own
composer, and tells you it's public before you type a word.

## What makes it different

Most feeds are full of people saying what they're *about* to do. "I'm going to learn Rust" is easy
to post and worth nothing.

Here, the thing that shows up in the feed is a **thread**: the original note, plus what actually
happened to it over time. When one resurfaces six weeks later carrying *"Done — here's what I
built"*, that's something no other feed has. Follow-through is the whole product, pointed inward
first and outward second.

---
📖 **[The longer version](docs/about.md)** — the idea, the sharing, accounts, privacy
· 📐 [What it is, precisely](docs/product.md) · 🔑 [Accounts setup](docs/auth.md)

## Status

Being built in the open, by one person, part-time. It runs, and it's already usable for notes.

**Two things are honest to say up front:** there's no server yet, so the social side runs against
local storage with a few sample authors standing in for other people. And syncing between devices
isn't built, so your notes live on the device that wrote them.

| | Rollout 1 — the notes app | |
|---|---|---|
| 0 | Foundations | ✅ done |
| 1 | A good local notes app | ✅ mostly there |
| 2 | Capture surfaces — share sheet, URL scheme | — |
| 3 | Follow-through — the differentiator | 🚧 notes come back; the four-answer card is next |
| 4 | Accounts and sync | 🚧 sign-in done, sync not |
| 5 | Import and expansion | — |
| 6 | Ship | — |

| | Rollout 2 — the social layer ([plan](docs/plan-rollout-2.md)) | |
|---|---|---|
| 2.0 | Identity — profiles and handles | ✅ done |
| 2.1 | Publishing | 🚧 works in the app; public web pages not built |
| 2.2 | Follows and a chronological feed | ✅ done |
| 2.3 | Conversation and safety | 🚧 likes, block and report done; comments not |
| 2.4 | Themes as data, wallpapers | — |
| 2.5 | Discovery | — |

Full plans: [docs/plan.md](docs/plan.md) · [docs/plan-rollout-2.md](docs/plan-rollout-2.md).
What the product *is*, as opposed to how it gets built: [docs/product.md](docs/product.md).

## Accounts

You don't need one to write notes. You never will — that isn't a trial, it's the design.

An account is asked for at exactly one moment: when you want to put something where other people
can read it. There are three ways in, and they all end up at the same account:

- **Google**
- **A six-digit code by email** — no password, so there's nothing to forget and nothing to leak
- **Sign in with Apple** — on iOS

Signing out leaves every note exactly where it is. They were never the account's.

Setting this up for your own build takes a few console steps: [docs/auth.md](docs/auth.md).

## Privacy

**This app does not use end-to-end encryption, and the server can read your notes.** Notes are
encrypted in transit and at rest. Any note can be marked **local-only**, and those never leave your
device: publishing one is refused in code, and the server schema carries a constraint that makes a
published local-only note impossible to store at all.

That's a deliberate trade: end-to-end encryption would permanently rule out server-side search. The
whole server is open source and self-hostable, which is our substitute for "trust us." See
[ADR 0002](docs/adr/0002-no-end-to-end-encryption.md).

There's also **no integration with Apple Notes or Google Keep**. Neither has an API to build
against, so it's dropped rather than promised
([ADR 0003](docs/adr/0003-no-notes-app-integrations.md)).

---
Built in the open, by one person, part-time. Usable for notes today.

## For developers
Two things to know before cloning: **there's no server yet**, so the social side runs against local
storage with a few sample authors standing in for other people, and **there's no sync**, so notes
live on the device that wrote them.

### Layout
**[Rollout 1 — the notes app](docs/plan.md)**

```
apps/mobile Expo app (expo-router, NativeWind) MIT
packages/core pure TS domain logic, no RN, no Supabase MIT
packages/db Drizzle schema + Postgres migrations AGPL-3.0
supabase migrations, RLS policies AGPL-3.0
docs product.md, the rollout plans, auth.md, ADRs
```
| ✅ | Foundations · a good local notes app |
|---|---|
| 🚧 | Follow-through (notes come back; the four-answer card is next) · accounts (sync is next) |
| — | Share sheet and URL scheme · import · ship |
Comment on lines +32 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use one rollout status for the four-answer workflow.

  • README.md#L32-L33: mark the four-answer card as shipped, or keep it as planned.
  • docs/about.md#L18-L29: match the selected status and label the review ladder accordingly.
📍 Affects 2 files
  • README.md#L32-L33 (this comment)
  • docs/about.md#L18-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 32 - 33, Use a single rollout status for the
four-answer workflow: in README.md lines 32-33, either mark the four-answer card
as shipped or retain it as planned; in docs/about.md lines 18-29, apply the same
status and update the review ladder label to match.


`packages/core` holds hashtag parsing, title derivation, the review scheduler, search ranking and
feed composition. These are the pieces most likely to have subtle bugs and the most annoying to
debug through a UI, so they're pure functions with unit tests and no simulator involved.
**[Rollout 2 — the social layer](docs/plan-rollout-2.md)**

Two seams worth knowing about:
| ✅ | Profiles and handles · follows and the feed |
|---|---|
| 🚧 | Publishing (works in-app; public web pages not built) · safety (no comments yet) |
| — | Themes and wallpapers · discovery |

- **`apps/mobile/src/db/social-repo.ts`** is the entire social storage layer. When there's a real
server, that one file gets reimplemented over HTTP and nothing else changes.
- **Feed rules are code, not database policy.** Row-level security answers one question — "is this
row public" — and following, blocking and ordering are decided in `packages/core/src/social.ts`,
where they can be tested.

### Getting started
## Getting started

```bash
npm install
npm start # Expo dev server
npm run android # or: npm run ios
```

Requires Node ≥ 20.19.

You'll need a **development build** rather than Expo Go — Google Sign-In is native code:
Requires Node ≥ 20.19, and a **development build** rather than Expo Go — Google Sign-In is native
code:

```bash
npx expo prebuild --clean
npx expo run:android
```

Sign-in needs credentials. Without them the app still launches, still takes notes and still reads
the feed; it just can't sign anyone in. Copy `apps/mobile/.env.example` to `.env.local` and see
[docs/auth.md](docs/auth.md).
the feed. Copy `apps/mobile/.env.example` to `.env.local` and see [docs/auth.md](docs/auth.md).

### Checks

Expand All @@ -174,7 +68,27 @@ npm run lint

All three run in CI on every PR.

### Database
## Layout

```
apps/mobile Expo app (expo-router, NativeWind) MIT
packages/core pure TS domain logic, no RN, no Supabase MIT
packages/db Drizzle schema + Postgres migrations AGPL-3.0
supabase migrations, RLS policies AGPL-3.0
docs about.md, product.md, the plans, auth.md, ADRs
```
Comment on lines +73 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the layout code fence.

Line 73 opens a fenced block without a language. Use text so markdownlint passes MD040.

Proposed fix
-```
+```text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
apps/mobile Expo app (expo-router, NativeWind) MIT
packages/core pure TS domain logic, no RN, no Supabase MIT
packages/db Drizzle schema + Postgres migrations AGPL-3.0
supabase migrations, RLS policies AGPL-3.0
docs about.md, product.md, the plans, auth.md, ADRs
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 73-73: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 73 - 79, Update the fenced code block containing the
repository package listing in README.md to specify the text language on its
opening fence, preserving the existing block contents.

Source: Linters/SAST tools


`packages/core` holds hashtag parsing, title derivation, the review scheduler, search ranking and
feed composition — the pieces most likely to have subtle bugs and the most annoying to debug
through a UI, so they're pure functions with unit tests and no simulator involved.

Two seams worth knowing about:

- **`apps/mobile/src/db/social-repo.ts`** is the entire social storage layer. When there's a real
server, that one file gets reimplemented over HTTP and nothing else changes.
- **Feed rules are code, not database policy.** Row-level security answers one question — "is this
row public" — while following, blocking and ordering are decided in
`packages/core/src/social.ts`, where they can be tested.

The app is local-only until sync lands, so nothing here touches Postgres day to day. To work on the
schema, see [supabase/README.md](supabase/README.md).
Expand Down
69 changes: 38 additions & 31 deletions apps/mobile/src/components/feed-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,30 +80,29 @@ export function FeedCard({
const name = item.displayName ?? `@${item.handle}`;
const completed = event === 'followed-through';

// Three sibling targets rather than one card with controls nested inside it:
// the byline goes to the author, the body goes to the thread, and the like
// acts in place. Nesting them made a button inside a button -- invalid on the
// web surface, and a screen reader announcing one row as a single control
// with mystery children on every platform.
return (
<View className="px-4">
<Pressable
accessibilityRole="button"
accessibilityLabel={`${name}, ${feedEventLabel(event)}: ${feedTitle(item)}`}
onPress={() => router.push({ pathname: '/post/[id]', params: { id: item.id } })}
<View
className={[
'bg-card px-4 active:opacity-70 dark:bg-card-dark',
'bg-card px-4 dark:bg-card-dark',
first ? 'rounded-t-2xl pt-4' : 'pt-3.5',
last ? 'rounded-b-2xl pb-3' : 'pb-3',
].join(' ')}
>
<View className="flex-row items-center gap-2.5">
<Pressable
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={`${name}'s profile`}
onPress={() =>
router.push({ pathname: '/profile/[handle]', params: { handle: item.handle } })
}
className="active:opacity-60"
>
<Avatar profile={item} size={34} />
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={`${name}, @${item.handle}`}
onPress={() =>
router.push({ pathname: '/profile/[handle]', params: { handle: item.handle } })
}
className="flex-row items-center gap-2.5 active:opacity-60"
>
<Avatar profile={item} size={34} />

<View className="flex-1">
<Text numberOfLines={1} className="text-[15px] text-ink dark:text-ink-dark">
Expand All @@ -118,22 +117,30 @@ export function FeedCard({
</Text>
</View>

{completed ? (
<Icon name="followedThrough" size={19} color={theme.accent} />
) : null}
</View>
{completed ? <Icon name="followedThrough" size={19} color={theme.accent} /> : null}
</Pressable>

<Text
numberOfLines={2}
className="pt-3 font-serif text-[19px] leading-6 text-ink dark:text-ink-dark"
<Pressable
accessibilityRole="button"
accessibilityLabel={`${feedEventLabel(event)}: ${feedTitle(item)}`}
onPress={() => router.push({ pathname: '/post/[id]', params: { id: item.id } })}
className="active:opacity-70"
>
{feedTitle(item)}
</Text>
{preview.length > 0 ? (
<Text numberOfLines={2} className="pt-1 text-[15px] leading-5 text-muted dark:text-muted-dark">
{preview}
<Text
numberOfLines={2}
className="pt-3 font-serif text-[19px] leading-6 text-ink dark:text-ink-dark"
>
{feedTitle(item)}
</Text>
) : null}
{preview.length > 0 ? (
<Text
numberOfLines={2}
className="pt-1 text-[15px] leading-5 text-muted dark:text-muted-dark"
>
{preview}
</Text>
) : null}
</Pressable>

<View className="flex-row items-center gap-4 pt-3">
<LikeButton item={item} onPress={onLike} />
Expand All @@ -146,7 +153,7 @@ export function FeedCard({
) : null}
{item.viewCount > 0 ? <Stat icon="views" label={String(item.viewCount)} /> : null}
</View>
</Pressable>
</View>

{!last ? <View className="mx-4 h-px bg-line dark:bg-line-dark" /> : null}
</View>
Expand Down
Loading
Loading