···17171818These are our first-party apps, built with Civility.
19192020-| Name | Description |
2121-| --------------------------------------------- | --------------------------------------------------------- |
2222-| [CookApp](https://git.sr.ht/~bpev/cook) | Store Cooking Recipes |
2323-| [ClimbApp](https://git.sr.ht/~bpev/climb) | An app for logging Climbing Sessions |
2424-| [FitApp](https://git.sr.ht/~bpev/fit) | Log Workouts |
2525-| [HanziApp](https://git.sr.ht/~bpev/hanzi) | Learn Chinese and Japanese Characters |
2626-| [JournalApp](https://git.sr.ht/~bpev/journal) | A Daily Journal |
2727-| [MapsApp](https://git.sr.ht/~bpev/maps) | An offline-capable maps app for logging where you've been |
2020+| Name | Description |
2121+| --------------------------------------------------------------------------------- | --------------------------------------------------------- |
2222+| [CookApp](https://cook.bpev.me) ([source](https://git.sr.ht/~bpev/cook)) | Store Cooking Recipes |
2323+| [ClimbApp](https://climb.bpev.me) ([source](https://tangled.org/bpev.me/climb)) | An app for logging Climbing Sessions |
2424+| [HanziApp](https://hanzi.bpev.me) ([source](https://tangled.org/bpev.me/hanzi)) | Learn Chinese and Japanese Characters |
2525+| [JournalApp](https://journal.bpev.me) ([source](https://git.sr.ht/~bpev/journal)) | A Daily Journal |
2626+| [MapsApp](https://maps.bpev.me) ([source](https://tangled.org/bpev.me/maps)) | An offline-capable maps app for logging where you've been |
+138
docs/how_to/add_email_provider.md
···11+# Adding an Email Provider to a cServer
22+33+Civility uses an `EmailAdapter` to send password reset and email verification links. Without one, links are logged to the server console instead — a workable fallback for self-hosted deployments with a trusted user base.
44+55+---
66+77+## 1. The interface
88+99+```ts
1010+interface EmailAdapter {
1111+ send(opts: {
1212+ to: string
1313+ subject: string
1414+ html: string
1515+ text?: string
1616+ }): Promise<void>
1717+}
1818+```
1919+2020+Implement this interface for any provider and pass it to `createServer`.
2121+2222+---
2323+2424+## 2. Wire it up
2525+2626+```ts
2727+// server.ts
2828+import { createServer } from '@civility/hono'
2929+3030+const kv = await Deno.openKv()
3131+3232+const app = await createServer({
3333+ kv,
3434+ email: myEmailAdapter, // see examples below
3535+})
3636+3737+Deno.serve(app.fetch)
3838+```
3939+4040+---
4141+4242+## 3. Provider examples
4343+4444+### Resend
4545+4646+```ts
4747+import type { EmailAdapter } from '@civility/hono'
4848+4949+function createResendAdapter(apiKey: string): EmailAdapter {
5050+ return {
5151+ async send({ to, subject, html, text }) {
5252+ const res = await fetch('https://api.resend.com/emails', {
5353+ method: 'POST',
5454+ headers: {
5555+ 'Authorization': `Bearer ${apiKey}`,
5656+ 'Content-Type': 'application/json',
5757+ },
5858+ body: JSON.stringify({
5959+ from: 'Civility <noreply@yourdomain.com>',
6060+ to,
6161+ subject,
6262+ html,
6363+ text,
6464+ }),
6565+ })
6666+ if (!res.ok) {
6767+ throw new Error(`Resend error: ${res.status} ${await res.text()}`)
6868+ }
6969+ },
7070+ }
7171+}
7272+7373+// Usage:
7474+const app = await createServer({
7575+ kv,
7676+ email: createResendAdapter(Deno.env.get('RESEND_API_KEY')!),
7777+})
7878+```
7979+8080+### Postmark
8181+8282+```ts
8383+import type { EmailAdapter } from '@civility/hono'
8484+8585+function createPostmarkAdapter(serverToken: string): EmailAdapter {
8686+ return {
8787+ async send({ to, subject, html, text }) {
8888+ const res = await fetch('https://api.postmarkapp.com/email', {
8989+ method: 'POST',
9090+ headers: {
9191+ 'Accept': 'application/json',
9292+ 'Content-Type': 'application/json',
9393+ 'X-Postmark-Server-Token': serverToken,
9494+ },
9595+ body: JSON.stringify({
9696+ From: 'noreply@yourdomain.com',
9797+ To: to,
9898+ Subject: subject,
9999+ HtmlBody: html,
100100+ TextBody: text,
101101+ }),
102102+ })
103103+ if (!res.ok) {
104104+ throw new Error(`Postmark error: ${res.status} ${await res.text()}`)
105105+ }
106106+ },
107107+ }
108108+}
109109+```
110110+111111+### SMTP (via a relay)
112112+113113+If your host provides SMTP, use Deno's `nodemailer` compat or a fetch-based SMTP relay. For most production deployments, a transactional email API (Resend, Postmark, SES) is simpler than raw SMTP.
114114+115115+---
116116+117117+## 4. Self-hosted / no email configured
118118+119119+When no `email` adapter is passed to `createServer`, reset and verification links are printed to stdout:
120120+121121+```
122122+[civility] Password reset link for user@example.com: https://your-server.com/reset-password?token=...
123123+[civility] Email verification link for user@example.com: https://your-server.com/verify-email?token=...
124124+```
125125+126126+The server operator can copy the link from logs and send it to the user out-of-band (Signal, email, etc.). Reset tokens expire after **1 hour**; verification tokens expire after **24 hours**.
127127+128128+---
129129+130130+## 5. Behaviour by feature
131131+132132+| Feature | With adapter | Without adapter |
133133+| ----------------------- | --------------------- | ---------------------------- |
134134+| **Password reset** | Email sent to user | Link logged to console |
135135+| **Email verification** | Email sent on signup | Accounts created as verified |
136136+| **Resend verification** | Email sent on request | Link logged to console |
137137+138138+When no adapter is configured, new accounts are marked as verified automatically so the verification banner never appears.
+79
docs/how_to/integrate_sync.md
···11+# Adding `@civility/sync` to a cApp
22+33+A guide for apps that already use `@civility/store` and want cross-device sync via `@civility/sync`.
44+55+---
66+77+## 1. Dependencies (`deno.json`)
88+99+```json
1010+{
1111+ "imports": {
1212+ "@civility/blobs": "jsr:@civility/blobs@^1.0.0-beta.1",
1313+ "@civility/store": "jsr:@civility/store@^1.0.0-beta.6",
1414+ "@civility/store/idb": "jsr:@civility/store@^1.0.0-beta.6/idb",
1515+ "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.7",
1616+ "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.1",
1717+ "@civility/hono": "jsr:@civility/hono@^1.0.0-beta.4"
1818+ }
1919+}
2020+```
2121+2222+---
2323+2424+## 2. Expose your Collection from the store
2525+2626+`Synced` wraps one or more `Collection<T>` instances directly. Expose the collection as a getter:
2727+2828+```ts
2929+// models/store.ts
3030+import { Collection } from '@civility/store'
3131+3232+export class MyStore {
3333+ #store = new Collection<MyEntity>(backend, { name: 'my-store', schema })
3434+3535+ /** Consumed by Synced */
3636+ get syncStore(): Collection<MyEntity> {
3737+ return this.#store
3838+ }
3939+}
4040+```
4141+4242+The collection `name` becomes the `storeName` key used on the server — keep it stable across releases.
4343+4444+---
4545+4646+## 3. Wire up `Synced` in your app model
4747+4848+```ts
4949+// models/app.ts
5050+import { Synced } from '@civility/sync'
5151+5252+export class App {
5353+ store = new MyStore()
5454+ synced = new Synced({
5555+ stores: [this.store.syncStore],
5656+ appId: 'my-app', // must match the app ID created in the sync dashboard
5757+ })
5858+5959+ dispose() {
6060+ this.synced.dispose()
6161+ }
6262+}
6363+```
6464+6565+---
6666+6767+## 4. Add the sync UI to your settings route
6868+6969+```ts
7070+// routes/settings.ts
7171+import '@civility/ui'
7272+7373+// In your template:
7474+html`
7575+ <ui-sync storage-key="my-app-sync" .synced="${app.synced}"></ui-sync>
7676+`
7777+```
7878+7979+`storage-key` namespaces the sync link in `indexedDB` — use a unique value per app so multiple cApps on the same origin don't collide.
+6-3
packages/hono/auth/adapters/deno-kv.ts
···581581 await emailAdapter.send({
582582 to: userEmail,
583583 subject: 'Reset your password',
584584- html: `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${link}">${link}</a></p><p>If you did not request a password reset, you can ignore this email.</p>`,
585585- text: `Reset your password: ${link}\n\nThis link expires in 1 hour. If you did not request a reset, ignore this email.`,
584584+ html:
585585+ `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${link}">${link}</a></p><p>If you did not request a password reset, you can ignore this email.</p>`,
586586+ text:
587587+ `Reset your password: ${link}\n\nThis link expires in 1 hour. If you did not request a reset, ignore this email.`,
586588 })
587589 } else {
588590 console.log(`[civility] Password reset link for ${userEmail}: ${link}`)
···600602 await emailAdapter.send({
601603 to: user.email,
602604 subject: 'Verify your email address',
603603- html: `<p>Click the link below to verify your email address.</p><p><a href="${link}">${link}</a></p>`,
605605+ html:
606606+ `<p>Click the link below to verify your email address.</p><p><a href="${link}">${link}</a></p>`,
604607 text: `Verify your email: ${link}`,
605608 })
606609 } else {