From fe3da770ed2bfa72caf4222f73d49bbcd18100c9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 22:03:58 +0000 Subject: [PATCH 1/3] Add Railway one-click deploy to the README and landing page. Link the official Deploy on Railway button so people can self-host from GitHub or Syncidian.com, and keep the /data volume step in the docs so users survive redeploys. Co-authored-by: Shangeeth Sivan --- .env.example | 2 +- AGENT.md | 2 +- README.md | 8 +++++++- docs/architecture.md | 2 +- docs/install-mobile.md | 2 +- internal/server/agents_test.go | 3 +++ internal/server/server_test.go | 4 ++++ internal/web/embed_test.go | 25 +++++++++++++++++++++++++ internal/web/static/agents/landing.md | 4 ++++ internal/web/static/index.html | 14 ++++++++++++++ 10 files changed, 61 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 6749360..b2deb4e 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/AGENT.md b/AGENT.md index 8e36f69..c84ed9d 100644 --- a/AGENT.md +++ b/AGENT.md @@ -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**. diff --git a/README.md b/README.md index 14819c8..b06d94a 100644 --- a/README.md +++ b/README.md @@ -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. --- @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md index 4a0c8bd..c5064a7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. --- diff --git a/docs/install-mobile.md b/docs/install-mobile.md index 8fe9dff..9bd8f56 100644 --- a/docs/install-mobile.md +++ b/docs/install-mobile.md @@ -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://`. diff --git a/internal/server/agents_test.go b/internal/server/agents_test.go index 4e66811..b5b2f3d 100644 --- a/internal/server/agents_test.go +++ b/internal/server/agents_test.go @@ -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") } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 419af55..c67d4ce 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -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`, + `railway.com/button.svg`, + `data-cta="railway"`, `syncidian-github-signin`, `Continue with GitHub`, `id="email-form"`, diff --git a/internal/web/embed_test.go b/internal/web/embed_test.go index 4194a07..830fb5d 100644 --- a/internal/web/embed_test.go +++ b/internal/web/embed_test.go @@ -41,3 +41,28 @@ 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, `https://railway.com/button.svg`) { + t.Fatal("landing is missing the Deploy on Railway button") + } + 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") + } +} diff --git a/internal/web/static/agents/landing.md b/internal/web/static/agents/landing.md index b5b5fb3..a12788f 100644 --- a/internal/web/static/agents/landing.md +++ b/internal/web/static/agents/landing.md @@ -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_…` diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 861c4c9..e196fa3 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -282,6 +282,12 @@ .hero-source { padding: 12px 20px; border-radius: 14px; font-size: 15px; text-transform: lowercase; } + .railway-deploy { + display: inline-flex; align-items: center; line-height: 0; + border-radius: 8px; overflow: hidden; + } + .railway-deploy img { height: 40px; width: auto; display: block; } + .railway-note { margin: 16px 0 0; font-size: 14px; } #landing .arch div { background: color-mix(in srgb, var(--card) 58%, transparent); backdrop-filter: blur(18px); @@ -613,11 +619,15 @@

Your data
stays with you.

+

Self-host in one click. Attach a /data volume so users survive redeploys.

Sign inGitHub.
Connect a repoInstall the GitHub App.
@@ -701,6 +711,7 @@

Ready to begin?

Privacy Story + Deploy on Railway Source
@@ -842,6 +853,7 @@

Setup walkthrough

const placement = el.getAttribute("data-placement") || ""; if (cta === "discord") track("click_discord", { placement }); else if (cta === "github") track("click_github", { placement }); + else if (cta === "railway") track("click_railway", { placement }); else if (cta === "github_signin") track("click_github_signin", { placement }); else if (cta === "waitlist") track("click_waitlist", { placement }); }); @@ -987,6 +999,8 @@

Setup walkthrough

The public site is a one-page story. On a self-hosted host, people sign in with email. On Syncidian.com they use GitHub (tap the app name six times to reveal it until public launch).

Self-host operators: open /admin on this same URL to create the first admin. Skip Tailscale — leave SYNCIDIAN_ADMIN_HOST unset, or set SYNCIDIAN_ADMIN_PRIVATE=0.

A Tailscale-only hostname (admin.syncidian.com) is optional. Tailscale’s DNS page has no Extra records control — use a hosts file, Split DNS + Pi-hole, a public A to 100.x on a self-hosted node, or /admin on Railway. See docs/tailscale-admin.md.

+

Railway (one click)

+

Deploy on Railway from this GitHub repo, then attach a volume at /data before you create the admin.

Docker

git clone https://github.com/shangeethsivan/Syncidian.git
 cd Syncidian

From 4f1f73cc9012eaffa4eb6988c8eb223b2a320bfb Mon Sep 17 00:00:00 2001
From: Cursor Agent 
Date: Wed, 9 Sep 2026 22:13:07 +0000
Subject: [PATCH 2/3] Serve the Railway deploy button from local assets.

Keep the official button artwork on the landing page even if railway.com is slow or blocked.

Co-authored-by: Shangeeth Sivan 
---
 internal/server/server_test.go                | 19 ++++++++++++++++++-
 internal/web/embed_test.go                    |  5 ++++-
 internal/web/static/assets/railway-button.svg |  1 +
 internal/web/static/index.html                |  2 +-
 4 files changed, 24 insertions(+), 3 deletions(-)
 create mode 100644 internal/web/static/assets/railway-button.svg

diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index c67d4ce..d7224a3 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -490,7 +490,7 @@ func TestDashboardServed(t *testing.T) {
 		`github-signin`,
 		`Deploy on Railway`,
 		`railway.com/new/template?template=https://github.com/shangeethsivan/Syncidian`,
-		`railway.com/button.svg`,
+		`/assets/railway-button.svg`,
 		`data-cta="railway"`,
 		`syncidian-github-signin`,
 		`Continue with GitHub`,
@@ -642,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("
\ No newline at end of file
diff --git a/internal/web/static/index.html b/internal/web/static/index.html
index e196fa3..a4d8e3b 100644
--- a/internal/web/static/index.html
+++ b/internal/web/static/index.html
@@ -620,7 +620,7 @@ 

Your data
stays with you.

- -

Self-host in one click. Attach a /data volume so users survive redeploys.

Sign inGitHub.
Connect a repoInstall the GitHub App.