5.6 KiB
synq, a tiny android-to-org capture system.
open app.
type, optionally mark as todo, optionally add tags, save.
if home server accessible, post unsynced captures to lan-only service and appended to synq.org.
non-goals
- no org parser on android
- no agenda on android
- no sync provider dependency
- no nextcloud file editing
- no conflict resolution
- no public internet exposure
- no ai tagging in v1
- no account system in v1
architecture
android app
kotlin + jetpack compose
room sqlite local queue
workmanager + manual sync
okhttp/retrofit client
home server
docker container on jeeves/unraid
python + fastapi
sqlite idempotency store
append-only org writer
flow
user saves capture locally
app marks it pending
sync runs when server is reachable
app posts pending captures to fastapi
server validates, dedupes by id, appends to phone.org
app marks capture synced
v1 behavior
the app launches into a focused text box. the user can type immediately.
controls:
- note/todo toggle
- optional tags field
- save
- save and close
- sync now
- small history view showing pending, synced, and failed entries
each capture has:
- stable client-generated id
- created timestamp with timezone
- kind:
noteortodo - body text
- tags
- device name
- sync status
- optional last error
org output
todo:
* TODO buy printer paper :home:errands:
:PROPERTIES:
:CREATED: [2026-05-17 sun 14:31]
:SOURCE: android
:ID: phone-20260517-143122-a8f2
:END:
note:
* note :retcon:
:PROPERTIES:
:CREATED: [2026-05-17 sun 14:33]
:SOURCE: android
:ID: phone-20260517-143322-b91c
:END:
mobile capture should stay dumb and append-only.
server paths
default container paths:
/data/synq.org
/data/capture.sqlite3
/data/rejected.log
recommended unraid host mapping:
/mnt/user/synq/phone-capture:/data
or map /data/synq.org directly to wherever the real org file lives. prefer a dedicated capture file first; emacs can include it in agenda later.
security model
v1 is lan-only. bind the service to the host lan and do not expose it through swag/cloudflare/public dns.
minimum useful controls:
- shared bearer token in app and server env
- server only accepts json
- server rejects empty body
- server dedupes ids
- server writes append-only org
- server never edits or parses existing org
- docker volume is backed up
repo layout
suggested monorepo:
synq/
android/
app/
server/
app/
main.py
models.py
org_writer.py
store.py
tests/
Dockerfile
pyproject.toml
docs/
api.md
android-notes.md
server-notes.md
docker-compose.example.yml
.env.example
tasks.org
README.md
build order
- implement server first using curl tests.
- implement android local capture with room.
- implement manual sync.
- add workmanager opportunistic sync.
- add history screen and resend handling.
- polish launch speed and widget/share-target only after core path works.
build & deploy
server (docker)
# build
docker build -t synq-server ./server
# run (replace paths and token as needed)
docker run -d --name synq --restart=unless-stopped \
-p 8765:8765 \
-v /mnt/user/synq/data:/data \
synq-server
the server generates a random token on first start and logs it prominently. copy it into the android app settings. to use a fixed token instead, pass -e PHONE_CAPTURE_TOKEN=yourtoken.
to rebuild after a git pull:
git pull
docker build -t synq-server ./server
docker stop synq && docker rm synq
# re-run the docker run command above
android
open android/ in Android Studio and hit Sync. then either:
- run on device/emulator directly from Android Studio (▶), or
- build a release APK from the terminal:
cd android
./gradlew assembleRelease
# output: app/build/outputs/apk/release/app-release-unsigned.apk
sideload to a connected device:
adb install app/build/outputs/apk/release/app-release-unsigned.apk
releasing on gitea
tag the commit and push the tag:
git tag v1.0.0
git push gitea v1.0.0
then go to Releases → New Release in the Gitea UI, pick the tag, and drag the APK into the assets box. anyone on the LAN can download it from there.
backup
what to back up
the server writes to one directory (the /data volume). back up the whole thing:
/data/synq.org ← the canonical org capture file
/data/capture.sqlite3 ← idempotency store (dedup ids)
/data/token.txt ← auto-generated token (if not using PHONE_CAPTURE_TOKEN env var)
on unraid the host path is whatever you mapped in compose, e.g. /mnt/user/ben/synq/phone-capture.
restore behavior
- restore the
/datavolume to a new container and start it. the server will pick up where it left off. synq.orgis append-only plain text — it is human-readable and recoverable even without the sqlite db.- if
capture.sqlite3is lost butsynq.orgis intact, the server will accept re-posted captures that were already in the org file (no dedup). the android app marks them synced either way (already_seenoraccepted), so the only side effect is duplicate org entries for anything re-synced. restore the db from backup to avoid this. - if
token.txtis lost, delete it and let the server generate a new one on next start, then update the app settings.
v2 parking lot
- android share target
- quick settings tile or widget
- tag chips from recent tags
- configurable default tag
- edit unsynced entries only
- multi-device capture
- wireguard-aware sync
- local export/import
- optional emacs ingest helpers