Recently Written · git

sbm-sync

Sync server for sbm bookmark files: one small Go program, plain files, AGPL

git clone https://github.com/equwal/sbm-sync

Log | Files | Refs


commit f37b3a0b1ab658f686cf0cf531404127b847a36b
equwal <truex@equwal.com>
2026-09-22 16:05:57 -0700

Sync is free, and the home page can link to a pre-order of teams

Sync on sbm.subread.space is free. The README no longer gives a price
for it.

SBM_TEAMS_URL is the address of a page that pre-sells team bookmarks,
for example a Stripe Payment Link. When it is set, the home page tells
about teams and links to that page. The server refuses to start when
the value is not an http or https address.

 README.md      |  5 +++--
 main.go        |  9 ++++++++-
 pages.html     |  4 ++++
 server_test.go | 11 +++++++++++
 4 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 91ae916..c608472 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,7 @@ It is one small Go program. It keeps its data in plain files: no database.
 The only dependency is `golang.org/x/crypto` for bcrypt.
 
 The clients use **https://sbm.subread.space** unless you set another
-server. That server is free for 30 days, then $3 a month or $30 a year.
-Run your own server free of charge.
+server. Sync on that server is free. You can also run your own server.
 
 ## How sync works
 
@@ -72,6 +71,8 @@ Settings, all through the environment:
     SBM_ADDR        address to listen on (default 127.0.0.1:8750)
     SBM_DATA        data directory (default ./data)
     SBM_CONTACT     email address on the privacy page (optional)
+    SBM_TEAMS_URL   page that pre-sells team bookmarks (optional): the
+                    home page links to it
     SBM_TRIAL_DAYS  days of sync before payment, with billing on (default 30)
     SBM_BILLING_START  date when billing starts, as 2026-10-01: accounts
                     from before it get the full trial from that date
diff --git a/main.go b/main.go
index 05f47b5..f84950a 100644
--- a/main.go
+++ b/main.go
@@ -41,6 +41,7 @@ type server struct {
 	trial   time.Duration
 	since   time.Time // start of billing: no trial ends before since + trial
 	contact string
+	teams   string            // address of the page that pre-sells team bookmarks, or ""
 	prices  map[string]string // plan ("month", "year"): Stripe price ID
 	labels  map[string]string // plan: price as text
 	limit   limiter
@@ -65,6 +66,10 @@ func main() {
 			log.Fatalf("SBM_BILLING_START: %v", err)
 		}
 	}
+	teams := os.Getenv("SBM_TEAMS_URL")
+	if t, err := url.Parse(teams); teams != "" && (err != nil || t.Host == "" || (t.Scheme != "https" && t.Scheme != "http")) {
+		log.Fatalf("SBM_TEAMS_URL: not an address: %q", teams)
+	}
 	store, err := openStore(env("SBM_DATA", "data"))
 	if err != nil {
 		log.Fatal(err)
@@ -77,6 +82,7 @@ func main() {
 		trial:   time.Duration(days) * 24 * time.Hour,
 		since:   since,
 		contact: os.Getenv("SBM_CONTACT"),
+		teams:   teams,
 		prices:  map[string]string{"month": os.Getenv("STRIPE_PRICE_MONTH"), "year": os.Getenv("STRIPE_PRICE_YEAR")},
 		labels:  map[string]string{"month": "monthly", "year": "yearly"},
 	}
@@ -165,6 +171,7 @@ func (s *server) routes() http.Handler {
 
 type page struct {
 	Title, Error, Email, State, URL, Contact string
+	Teams                                    string // address of the pre-order page
 	Month, Year                              string
 	TrialDays                                int
 	Billing, CanSubscribe, Unconfirmed       bool
@@ -172,7 +179,7 @@ type page struct {
 }
 
 func (s *server) page(title string) page {
-	return page{Title: title, URL: s.site, Contact: s.contact, Billing: s.stripe != nil,
+	return page{Title: title, URL: s.site, Contact: s.contact, Teams: s.teams, Billing: s.stripe != nil,
 		Month: s.labels["month"], Year: s.labels["year"], TrialDays: int(s.trial.Hours() / 24)}
 }
 
diff --git a/pages.html b/pages.html
index efe47ce..2e9b62f 100644
--- a/pages.html
+++ b/pages.html
@@ -46,6 +46,10 @@ your computers, the sbm app for Android, and the sbm add-on for Firefox and
 Chrome.</p>
 {{if .Billing}}<p>Free for {{.TrialDays}} days. Then {{.Year}} or {{.Month}}. Cancel at any time.</p>{{end}}
 <p>{{if .Email}}<a href="/account">Your account</a>{{else}}<a href="/signup">Create an account</a> or <a href="/login">sign in</a>.{{end}}</p>
+{{with .Teams}}<h2>Teams</h2>
+<p>Coming: bookmarks for teams. The members of a team share one bookmark
+file, and sync it on each of their devices. <a href="{{.}}">Pre-order the
+founding plan</a>.</p>{{end}}
 <p class="note">sbm Sync is free software under the AGPL-3.0. You can
 <a href="https://github.com/equwal/sbm-sync">run your own server</a>.</p>
 {{template "foot" .}}{{end}}
diff --git a/server_test.go b/server_test.go
index 197a714..fc04d93 100644
--- a/server_test.go
+++ b/server_test.go
@@ -498,6 +498,17 @@ func TestOldVersionsGo(t *testing.T) {
 	}
 }
 
+func TestTeamsPreorderLink(t *testing.T) {
+	e := newEnv(t, false)
+	if strings.Contains(e.body(http.DefaultClient, "/"), "Pre-order") {
+		t.Error("the home page offers teams without SBM_TEAMS_URL")
+	}
+	e.s.teams = "https://buy.stripe.com/test_x"
+	if !strings.Contains(e.body(http.DefaultClient, "/"), `<a href="https://buy.stripe.com/test_x">Pre-order`) {
+		t.Error("the home page has no pre-order link")
+	}
+}
+
 func TestEmailCheck(t *testing.T) {
 	e := newEnv(t, false)
 	e.withMail()