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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ SYNCIDIAN_PUBLIC_URL=http://localhost:8080
# SYNCIDIAN_GITHUB_ALLOWED_EMAILS=["Shangeeth95@gmail.com"]

# Google Analytics 4 measurement ID for the public landing (hosted). Empty = off.
# Events: click_discord, click_github, join_waitlist, click_github_signin.
# Events: click_discord, click_github, click_railway, join_waitlist, click_github_signin.
# SYNCIDIAN_GA_ID=G-XXXXXXXXXX
2 changes: 1 addition & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ When you change auth, GitHub scoping, admin privacy, operator hostname/Tailscale

## Help walkthrough

The dashboard **Help** button must describe the same workflow as the diagrams: public landing with waitlist + GitHub on hosted Syncidian.com (GitHub hidden until the app name is tapped six times), email signup/login on self-hosted hosts, self-host operators at `/admin` (skip Tailscale with `SYNCIDIAN_ADMIN_PRIVATE=0` or unset `SYNCIDIAN_ADMIN_HOST`), optional Tailscale hostname, persist `/data` so deploys do not wipe users, optional instance GitHub App (`docs/github-app.md`) for GitHub sign-in or backup, then optional per-user GitHub, admin without repo sync. Plugin install is Community plugins first, with BRAT or `scripts/install-plugin.sh` as fallback until the listing is live. Dashboard MCP/call/conflict stats stay behind **Stats for Nerds**.
The dashboard **Help** button must describe the same workflow as the diagrams: public landing with waitlist + GitHub on hosted Syncidian.com (GitHub hidden until the app name is tapped six times), email signup/login on self-hosted hosts, self-host via the Railway one-click button or Docker, operators at `/admin` (skip Tailscale with `SYNCIDIAN_ADMIN_PRIVATE=0` or unset `SYNCIDIAN_ADMIN_HOST`), optional Tailscale hostname, persist `/data` so deploys do not wipe users, optional instance GitHub App (`docs/github-app.md`) for GitHub sign-in or backup, then optional per-user GitHub, admin without repo sync. Plugin install is Community plugins first, with BRAT or `scripts/install-plugin.sh` as fallback until the listing is live. Dashboard MCP/call/conflict stats stay behind **Stats for Nerds**.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ docker run -d \

**Self-host Syncidian for free.** A managed service is planned at $1/month; it is not available yet.

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian&utm_medium=integration&utm_source=button&utm_campaign=syncidian)

One click deploys this repo on [Railway](https://railway.com). Attach a volume at `/data` before you create the admin so users survive redeploys. Details: [Deploy on Railway](#deploy-on-railway).

[Buy me a coffee](https://buymeacoffee.com/shravzdev) if Syncidian is useful.

---
Expand Down Expand Up @@ -67,9 +71,11 @@ Data is stored in `./data` by default (`SYNCIDIAN_DATA` to change it). Docker Co

### Deploy on Railway

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian&utm_medium=integration&utm_source=button&utm_campaign=syncidian)

Users, GitHub App credentials, and vault files live in SQLite on disk. Railway’s container filesystem is **empty on every deploy** unless you attach a volume.

1. New project → deploy this GitHub repo. Railway builds the `Dockerfile`.
1. Click **Deploy on Railway** (or New project → this GitHub repo). Railway clones the repo and builds the `Dockerfile` from `railway.json`.
2. **Settings → Volumes → Add volume**, mount path **`/data`**. Do this before creating the admin or registering the GitHub App. `railway.json` sets `requiredMountPath` to `/data` so a deploy without a volume fails instead of silently resetting the instance, and `overlapSeconds` to `0` so two replicas do not share SQLite during a rollout.
3. Generate a public domain. The server listens on Railway’s `PORT` and uses `RAILWAY_PUBLIC_DOMAIN` for the dashboard URL unless you set `SYNCIDIAN_PUBLIC_URL`.
4. Optional variables: `SYNCIDIAN_BOOTSTRAP_USER`, `SYNCIDIAN_BOOTSTRAP_PASSWORD`. To keep the GitHub App if the volume is missing, also set `SYNCIDIAN_GITHUB_APP_*` (see [Set up the GitHub App](docs/github-app.md)). Set `SYNCIDIAN_DATA_KEY` (32-byte hex) so GitHub App secrets stay encrypted even if `syncidian.db` is copied off the volume. Until public GitHub sign-in, set `SYNCIDIAN_GITHUB_ALLOWED_EMAILS` to the GitHub emails that may complete OAuth (JSON array, commas, or one per line). Hosted should include `Shangeeth95@gmail.com`. Joining the waitlist does not grant access — add each person to this variable, then redeploy.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ One process. No extra services for the basic install. When `SYNCIDIAN_ADMIN_LIST

Users, tokens, the instance GitHub App, per-user GitHub installs, and vault files all live under `SYNCIDIAN_DATA` (SQLite `syncidian.db` plus `vaults/`). A new deploy **replaces the container filesystem**, so that directory must be a named volume.

Railway: mount a volume at `/data`. `railway.json` sets `requiredMountPath` to `/data` (deploys without a volume fail instead of wiping the instance) and `overlapSeconds` to `0` (SQLite is not opened by two replicas during a rollout). The image default `SYNCIDIAN_DATA=/data` no longer hides `RAILWAY_VOLUME_MOUNT_PATH` if the volume is mounted somewhere else. The Dockerfile does not declare `VOLUME` (that broke some builders). `/admin` and `GET /api/v1/setup` report when the data directory looks ephemeral. Leave `SYNCIDIAN_ADMIN_LISTEN_IP` unset on Railway so the public `PORT` still binds as today.
Railway: the README and landing page link [Deploy on Railway](https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian&utm_medium=integration&utm_source=button&utm_campaign=syncidian). After that click, mount a volume at `/data`. `railway.json` sets `requiredMountPath` to `/data` (deploys without a volume fail instead of wiping the instance) and `overlapSeconds` to `0` (SQLite is not opened by two replicas during a rollout). The image default `SYNCIDIAN_DATA=/data` no longer hides `RAILWAY_VOLUME_MOUNT_PATH` if the volume is mounted somewhere else. The Dockerfile does not declare `VOLUME` (that broke some builders). `/admin` and `GET /api/v1/setup` report when the data directory looks ephemeral. Leave `SYNCIDIAN_ADMIN_LISTEN_IP` unset on Railway so the public `PORT` still binds as today.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/install-mobile.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The Syncidian plugin is **not desktop-only**. The same `syncidian` plugin runs in Obsidian on Windows, macOS, Linux, Android, and iOS.

You still need a running Syncidian **server** (Railway, Docker, or similar) and a vault-user token (`sk_sync_…`) from that server’s dashboard. The plugin never talks to GitHub itself.
You still need a running Syncidian **server** ([Deploy on Railway](https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian&utm_medium=integration&utm_source=button&utm_campaign=syncidian), Docker, or similar) and a vault-user token (`sk_sync_…`) from that server’s dashboard. The plugin never talks to GitHub itself.

On a phone, leave **Syncidian.com** selected or choose **Custom Domain** with a public `https://` address (for example your Railway domain). `http://localhost:8080` is the phone, not your computer. iOS often blocks plain `http://`.

Expand Down
3 changes: 3 additions & 0 deletions internal/server/agents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ func TestAgentDiscovery(t *testing.T) {
if !strings.Contains(string(body), "MCP JSON-RPC") {
t.Fatalf("landing markdown: %s", body)
}
if !strings.Contains(string(body), "railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian") {
t.Fatalf("landing markdown missing Railway one-click deploy: %s", body)
}
if res.Header.Get("Link") == "" {
t.Fatal("markdown response missing Link")
}
Expand Down
21 changes: 21 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,10 @@ func TestDashboardServed(t *testing.T) {
`id="landing"`,
`id="landing-brand"`,
`github-signin`,
`Deploy on Railway`,
`railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian`,
`/assets/railway-button.svg`,
`data-cta="railway"`,
`syncidian-github-signin`,
`Continue with GitHub`,
`id="email-form"`,
Expand Down Expand Up @@ -638,6 +642,23 @@ func TestAdminChangePassword(t *testing.T) {
}
}

func TestRailwayButtonAsset(t *testing.T) {
hs, done := newTestServer(t)
defer done()
res, err := http.Get(hs.URL + "/assets/railway-button.svg")
if err != nil {
t.Fatal(err)
}
b, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("railway button status %d", res.StatusCode)
}
if !bytes.Contains(b, []byte("Deploy on Railway")) && !bytes.Contains(b, []byte("<svg")) {
t.Fatalf("railway button is not an svg: %q", b[:min(80, len(b))])
}
}

func TestFaviconServesLogo(t *testing.T) {
hs, done := newTestServer(t)
defer done()
Expand Down
28 changes: 28 additions & 0 deletions internal/web/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,31 @@ func TestLandingUsesAppLogo(t *testing.T) {
t.Fatal("landing still uses the placeholder brand dot")
}
}

func TestLandingHasRailwayDeploy(t *testing.T) {
b, err := FS.ReadFile("static/index.html")
if err != nil {
t.Fatal(err)
}
html := string(b)
const deploy = `https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian`
if !strings.Contains(html, deploy) {
t.Fatal("landing is missing the Railway one-click deploy URL")
}
if !strings.Contains(html, `/assets/railway-button.svg`) {
t.Fatal("landing is missing the Deploy on Railway button")
}
if _, err := FS.ReadFile("static/assets/railway-button.svg"); err != nil {
t.Fatal("web assets must include railway-button.svg")
}
if !strings.Contains(html, `data-cta="railway"`) {
t.Fatal("landing is missing Railway CTA tracking")
}
md, err := FS.ReadFile("static/agents/landing.md")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(md), deploy) {
t.Fatal("landing markdown is missing the Railway one-click deploy URL")
}
}
4 changes: 4 additions & 0 deletions internal/web/static/agents/landing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Syncidian is a self-hosted sync server and MCP bridge for Obsidian. The same plu
- **GitHub (Syncidian.com, after revealing it):** [Continue with GitHub](/api/v1/auth/github/start?next=install)
- **Email (self-host):** use the email form on the [homepage](/). You do not need a GitHub App to create an account. The operator still creates the first admin at `/admin`.

## Self-host on Railway

One-click deploy: [Deploy on Railway](https://railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian&utm_medium=integration&utm_source=button&utm_campaign=syncidian). Attach a volume at `/data` before creating the admin. `railway.json` requires that mount so a deploy without persistence fails instead of wiping users.

## For AI agents

- MCP JSON-RPC: `POST /mcp` with `Authorization: Bearer sk_sync_…`
Expand Down
1 change: 1 addition & 0 deletions internal/web/static/assets/railway-button.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading