···4343 page to complete your registration.
4444 </span>
4545 <div class="w-full mt-4 text-center">
4646- <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
4646+ <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
4747 </div>
4848 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
4949 <span>join now</span>
5050 </button>
5151+ <p class="text-sm text-gray-500">
5252+ Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
5353+ </p>
5454+5555+ <p id="signup-msg" class="error w-full"></p>
5656+ <p class="text-sm text-gray-500 pt-4">
5757+ By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>.
5858+ </p>
5159 </form>
5252- <p class="text-sm text-gray-500">
5353- Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
5454- </p>
5555-5656- <p id="signup-msg" class="error w-full"></p>
5760 </main>
5861 </body>
5962 </html>
+8
appview/pulls/pulls.go
···13661366 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
13671367 return
13681368 }
13691369+13691370 }
1370137113711372 if err = tx.Commit(); err != nil {
13721373 log.Println("failed to create pull request", err)
13731374 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
13741375 return
13761376+ }
13771377+13781378+ // notify about each pull
13791379+ //
13801380+ // this is performed after tx.Commit, because it could result in a locked DB otherwise
13811381+ for _, p := range stack {
13821382+ s.notifier.NewPull(r.Context(), p)
13751383 }
1376138413771385 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
+17
appview/state/git_http.go
···25252626}
27272828+func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
2929+ user, ok := r.Context().Value("resolvedId").(identity.Identity)
3030+ if !ok {
3131+ http.Error(w, "failed to resolve user", http.StatusInternalServerError)
3232+ return
3333+ }
3434+ repo := r.Context().Value("repo").(*models.Repo)
3535+3636+ scheme := "https"
3737+ if s.config.Core.Dev {
3838+ scheme = "http"
3939+ }
4040+4141+ targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
4242+ s.proxyRequest(w, r, targetURL)
4343+}
4444+2845func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
2946 user, ok := r.Context().Value("resolvedId").(identity.Identity)
3047 if !ok {
+1
appview/state/router.go
···101101102102 // These routes get proxied to the knot
103103 r.Get("/info/refs", s.InfoRefs)
104104+ r.Post("/git-upload-archive", s.UploadArchive)
104105 r.Post("/git-upload-pack", s.UploadPack)
105106 r.Post("/git-receive-pack", s.ReceivePack)
106107
+1530
docs/DOCS.md
···11+---
22+title: Tangled Documentation
33+author: The Tangled Contributors
44+date: 21 Sun, Dec 2025
55+---
66+77+# Introduction
88+99+Tangled is a decentralized code hosting and collaboration
1010+platform. Every component of Tangled is open-source and
1111+selfhostable. [tangled.org](https://tangled.org) also
1212+provides hosting and CI services that are free to use.
1313+1414+There are several models for decentralized code
1515+collaboration platforms, ranging from ActivityPub’s
1616+(Forgejo) federated model, to Radicle’s entirely P2P model.
1717+Our approach attempts to be the best of both worlds by
1818+adopting atproto—a protocol for building decentralized
1919+social applications with a central identity
2020+2121+Our approach to this is the idea of “knots”. Knots are
2222+lightweight, headless servers that enable users to host Git
2323+repositories with ease. Knots are designed for either single
2424+or multi-tenant use which is perfect for self-hosting on a
2525+Raspberry Pi at home, or larger “community” servers. By
2626+default, Tangled provides managed knots where you can host
2727+your repositories for free.
2828+2929+The "appview" at tangled.org acts as a consolidated “view”
3030+into the whole network, allowing users to access, clone and
3131+contribute to repositories hosted across different knots
3232+seamlessly.
3333+3434+# Quick Start Guide
3535+3636+## Login or Sign up
3737+3838+You can [login](https://tangled.org) by using your AT
3939+account. If you are unclear on what that means, simply head
4040+to the [signup](https://tangled.org/signup) page and create
4141+an account. By doing so, you will be choosing Tangled as
4242+your account provider (you will be granted a handle of the
4343+form `user.tngl.sh`).
4444+4545+In the AT network, users are free to choose their account
4646+provider (known as a "Personal Data Service", or PDS), and
4747+login to applications that support AT accounts.
4848+4949+You can think of it as "one account for all of the
5050+atmosphere"!
5151+5252+If you already have an AT account (you may have one if you
5353+signed up to Bluesky, for example), you can login with the
5454+same handle on Tangled (so just use `user.bsky.social` on
5555+the login page).
5656+5757+## Add an SSH Key
5858+5959+Once you are logged in, you can start creating repositories
6060+and pushing code. Tangled supports pushing git repositories
6161+over SSH.
6262+6363+First, you'll need to generate an SSH key if you don't
6464+already have one:
6565+6666+```bash
6767+ssh-keygen -t ed25519 -C "foo@bar.com"
6868+```
6969+7070+When prompted, save the key to the default location
7171+(`~/.ssh/id_ed25519`) and optionally set a passphrase.
7272+7373+Copy your public key to your clipboard:
7474+7575+```bash
7676+# on X11
7777+cat ~/.ssh/id_ed25519.pub | xclip -sel c
7878+7979+# on wayland
8080+cat ~/.ssh/id_ed25519.pub | wl-copy
8181+8282+# on macos
8383+cat ~/.ssh/id_ed25519.pub | pbcopy
8484+```
8585+8686+Now, navigate to 'Settings' -> 'Keys' and hit 'Add Key',
8787+paste your public key, give it a descriptive name, and hit
8888+save.
8989+9090+## Create a Repository
9191+9292+Once your SSH key is added, create your first repository:
9393+9494+1. Hit the green `+` icon on the topbar, and select
9595+ repository
9696+2. Enter a repository name
9797+3. Add a description
9898+4. Choose a knotserver to host this repository on
9999+5. Hit create
100100+101101+"Knots" are selfhostable, lightweight git servers that can
102102+host your repository. Unlike traditional code forges, your
103103+code can live on any server. Read the [Knots](TODO) section
104104+for more.
105105+106106+## Configure SSH
107107+108108+To ensure Git uses the correct SSH key and connects smoothly
109109+to Tangled, add this configuration to your `~/.ssh/config`
110110+file:
111111+112112+```
113113+Host tangled.org
114114+ Hostname tangled.org
115115+ User git
116116+ IdentityFile ~/.ssh/id_ed25519
117117+ AddressFamily inet
118118+```
119119+120120+This tells SSH to use your specific key when connecting to
121121+Tangled and prevents authentication issues if you have
122122+multiple SSH keys.
123123+124124+Note that this configuration only works for knotservers that
125125+are hosted by tangled.org. If you use a custom knot, refer
126126+to the [Knots](TODO) section.
127127+128128+## Push Your First Repository
129129+130130+Initialize a new git repository:
131131+132132+```bash
133133+mkdir my-project
134134+cd my-project
135135+136136+git init
137137+echo "# My Project" > README.md
138138+```
139139+140140+Add some content and push!
141141+142142+```bash
143143+git add README.md
144144+git commit -m "Initial commit"
145145+git remote add origin git@tangled.org:user.tngl.sh/my-project
146146+git push -u origin main
147147+```
148148+149149+That's it! Your code is now hosted on Tangled.
150150+151151+## Migrating an existing repository
152152+153153+Moving your repositories from GitHub, GitLab, Bitbucket, or
154154+any other Git forge to Tangled is straightforward. You'll
155155+simply change your repository's remote URL. At the moment,
156156+Tangled does not have any tooling to migrate data such as
157157+GitHub issues or pull requests.
158158+159159+First, create a new repository on tangled.org as described
160160+in the [Quick Start Guide](#create-a-repository).
161161+162162+Navigate to your existing local repository:
163163+164164+```bash
165165+cd /path/to/your/existing/repo
166166+```
167167+168168+You can inspect your existing git remote like so:
169169+170170+```bash
171171+git remote -v
172172+```
173173+174174+You'll see something like:
175175+176176+```
177177+origin git@github.com:username/my-project (fetch)
178178+origin git@github.com:username/my-project (push)
179179+```
180180+181181+Update the remote URL to point to tangled:
182182+183183+```bash
184184+git remote set-url origin git@tangled.org:user.tngl.sh/my-project
185185+```
186186+187187+Verify the change:
188188+189189+```bash
190190+git remote -v
191191+```
192192+193193+You should now see:
194194+195195+```
196196+origin git@tangled.org:user.tngl.sh/my-project (fetch)
197197+origin git@tangled.org:user.tngl.sh/my-project (push)
198198+```
199199+200200+Push all your branches and tags to tangled:
201201+202202+```bash
203203+git push -u origin --all
204204+git push -u origin --tags
205205+```
206206+207207+Your repository is now migrated to Tangled! All commit
208208+history, branches, and tags have been preserved.
209209+210210+## Mirroring a repository to Tangled
211211+212212+If you want to maintain your repository on multiple forges
213213+simultaneously, for example, keeping your primary repository
214214+on GitHub while mirroring to Tangled for backup or
215215+redundancy, you can do so by adding multiple remotes.
216216+217217+You can configure your local repository to push to both
218218+Tangled and, say, GitHub. You may already have the following
219219+setup:
220220+221221+```
222222+$ git remote -v
223223+origin git@github.com:username/my-project (fetch)
224224+origin git@github.com:username/my-project (push)
225225+```
226226+227227+Now add Tangled as an additional push URL to the same
228228+remote:
229229+230230+```bash
231231+git remote set-url --add --push origin git@tangled.org:user.tngl.sh/my-project
232232+```
233233+234234+You also need to re-add the original URL as a push
235235+destination (git replaces the push URL when you use `--add`
236236+the first time):
237237+238238+```bash
239239+git remote set-url --add --push origin git@github.com:username/my-project
240240+```
241241+242242+Verify your configuration:
243243+244244+```
245245+$ git remote -v
246246+origin git@github.com:username/repo (fetch)
247247+origin git@tangled.org:username/my-project (push)
248248+origin git@github.com:username/repo (push)
249249+```
250250+251251+Notice that there's one fetch URL (the primary remote) and
252252+two push URLs. Now, whenever you push, git will
253253+automatically push to both remotes:
254254+255255+```bash
256256+git push origin main
257257+```
258258+259259+This single command pushes your `main` branch to both GitHub
260260+and Tangled simultaneously.
261261+262262+To push all branches and tags:
263263+264264+```bash
265265+git push origin --all
266266+git push origin --tags
267267+```
268268+269269+If you prefer more control over which remote you push to,
270270+you can maintain separate remotes:
271271+272272+```bash
273273+git remote add github git@github.com:username/my-project
274274+git remote add tangled git@tangled.org:username/my-project
275275+```
276276+277277+Then push to each explicitly:
278278+279279+```bash
280280+git push github main
281281+git push tangled main
282282+```
283283+284284+# Knot self-hosting guide
285285+286286+So you want to run your own knot server? Great! Here are a few prerequisites:
287287+288288+1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind.
289289+2. A (sub)domain name. People generally use `knot.example.com`.
290290+3. A valid SSL certificate for your domain.
291291+292292+## NixOS
293293+294294+Refer to the [knot
295295+module](https://tangled.org/tangled.org/core/blob/master/nix/modules/knot.nix)
296296+for a full list of options. Sample configurations:
297297+298298+- [The test VM](https://tangled.org/tangled.org/core/blob/master/nix/vm.nix#L85)
299299+- [@pyrox.dev/nix](https://tangled.org/pyrox.dev/nix/blob/d19571cc1b5fe01035e1e6951ec8cf8a476b4dee/hosts/marvin/services/tangled.nix#L15-25)
300300+301301+## Docker
302302+303303+Refer to
304304+[@tangled.org/knot-docker](https://tangled.sh/@tangled.sh/knot-docker).
305305+Note that this is community maintained.
306306+307307+## Manual setup
308308+309309+First, clone this repository:
310310+311311+```
312312+git clone https://tangled.org/@tangled.org/core
313313+```
314314+315315+Then, build the `knot` CLI. This is the knot administration
316316+and operation tool. For the purpose of this guide, we're
317317+only concerned with these subcommands:
318318+319319+ * `knot server`: the main knot server process, typically
320320+ run as a supervised service
321321+ * `knot guard`: handles role-based access control for git
322322+ over SSH (you'll never have to run this yourself)
323323+ * `knot keys`: fetches SSH keys associated with your knot;
324324+ we'll use this to generate the SSH
325325+ `AuthorizedKeysCommand`
326326+327327+```
328328+cd core
329329+export CGO_ENABLED=1
330330+go build -o knot ./cmd/knot
331331+```
332332+333333+Next, move the `knot` binary to a location owned by `root` --
334334+`/usr/local/bin/` is a good choice. Make sure the binary itself is also owned by `root`:
335335+336336+```
337337+sudo mv knot /usr/local/bin/knot
338338+sudo chown root:root /usr/local/bin/knot
339339+```
340340+341341+This is necessary because SSH `AuthorizedKeysCommand` requires [really
342342+specific permissions](https://stackoverflow.com/a/27638306). The
343343+`AuthorizedKeysCommand` specifies a command that is run by `sshd` to
344344+retrieve a user's public SSH keys dynamically for authentication. Let's
345345+set that up.
346346+347347+```
348348+sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
349349+Match User git
350350+ AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys
351351+ AuthorizedKeysCommandUser nobody
352352+EOF
353353+```
354354+355355+Then, reload `sshd`:
356356+357357+```
358358+sudo systemctl reload ssh
359359+```
360360+361361+Next, create the `git` user. We'll use the `git` user's home directory
362362+to store repositories:
363363+364364+```
365365+sudo adduser git
366366+```
367367+368368+Create `/home/git/.knot.env` with the following, updating the values as
369369+necessary. The `KNOT_SERVER_OWNER` should be set to your
370370+DID, you can find your DID in the [Settings](https://tangled.sh/settings) page.
371371+372372+```
373373+KNOT_REPO_SCAN_PATH=/home/git
374374+KNOT_SERVER_HOSTNAME=knot.example.com
375375+APPVIEW_ENDPOINT=https://tangled.sh
376376+KNOT_SERVER_OWNER=did:plc:foobar
377377+KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
378378+KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
379379+```
380380+381381+If you run a Linux distribution that uses systemd, you can use the provided
382382+service file to run the server. Copy
383383+[`knotserver.service`](/systemd/knotserver.service)
384384+to `/etc/systemd/system/`. Then, run:
385385+386386+```
387387+systemctl enable knotserver
388388+systemctl start knotserver
389389+```
390390+391391+The last step is to configure a reverse proxy like Nginx or Caddy to front your
392392+knot. Here's an example configuration for Nginx:
393393+394394+```
395395+server {
396396+ listen 80;
397397+ listen [::]:80;
398398+ server_name knot.example.com;
399399+400400+ location / {
401401+ proxy_pass http://localhost:5555;
402402+ proxy_set_header Host $host;
403403+ proxy_set_header X-Real-IP $remote_addr;
404404+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
405405+ proxy_set_header X-Forwarded-Proto $scheme;
406406+ }
407407+408408+ # wss endpoint for git events
409409+ location /events {
410410+ proxy_set_header X-Forwarded-For $remote_addr;
411411+ proxy_set_header Host $http_host;
412412+ proxy_set_header Upgrade websocket;
413413+ proxy_set_header Connection Upgrade;
414414+ proxy_pass http://localhost:5555;
415415+ }
416416+ # additional config for SSL/TLS go here.
417417+}
418418+419419+```
420420+421421+Remember to use Let's Encrypt or similar to procure a certificate for your
422422+knot domain.
423423+424424+You should now have a running knot server! You can finalize
425425+your registration by hitting the `verify` button on the
426426+[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
427427+a record on your PDS to announce the existence of the knot.
428428+429429+### Custom paths
430430+431431+(This section applies to manual setup only. Docker users should edit the mounts
432432+in `docker-compose.yml` instead.)
433433+434434+Right now, the database and repositories of your knot lives in `/home/git`. You
435435+can move these paths if you'd like to store them in another folder. Be careful
436436+when adjusting these paths:
437437+438438+* Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent
439439+any possible side effects. Remember to restart it once you're done.
440440+* Make backups before moving in case something goes wrong.
441441+* Make sure the `git` user can read and write from the new paths.
442442+443443+#### Database
444444+445445+As an example, let's say the current database is at `/home/git/knotserver.db`,
446446+and we want to move it to `/home/git/database/knotserver.db`.
447447+448448+Copy the current database to the new location. Make sure to copy the `.db-shm`
449449+and `.db-wal` files if they exist.
450450+451451+```
452452+mkdir /home/git/database
453453+cp /home/git/knotserver.db* /home/git/database
454454+```
455455+456456+In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to
457457+the new file path (_not_ the directory):
458458+459459+```
460460+KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db
461461+```
462462+463463+#### Repositories
464464+465465+As an example, let's say the repositories are currently in `/home/git`, and we
466466+want to move them into `/home/git/repositories`.
467467+468468+Create the new folder, then move the existing repositories (if there are any):
469469+470470+```
471471+mkdir /home/git/repositories
472472+# move all DIDs into the new folder; these will vary for you!
473473+mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories
474474+```
475475+476476+In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH`
477477+to the new directory:
478478+479479+```
480480+KNOT_REPO_SCAN_PATH=/home/git/repositories
481481+```
482482+483483+Similarly, update your `sshd` `AuthorizedKeysCommand` to use the updated
484484+repository path:
485485+486486+```
487487+sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
488488+Match User git
489489+ AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories
490490+ AuthorizedKeysCommandUser nobody
491491+EOF
492492+```
493493+494494+Make sure to restart your SSH server!
495495+496496+#### MOTD (message of the day)
497497+498498+To configure the MOTD used ("Welcome to this knot!" by default), edit the
499499+`/home/git/motd` file:
500500+501501+```
502502+printf "Hi from this knot!\n" > /home/git/motd
503503+```
504504+505505+Note that you should add a newline at the end if setting a non-empty message
506506+since the knot won't do this for you.
507507+508508+# Spindles
509509+510510+## Pipelines
511511+512512+Spindle workflows allow you to write CI/CD pipelines in a
513513+simple format. They're located in the `.tangled/workflows`
514514+directory at the root of your repository, and are defined
515515+using YAML.
516516+517517+The fields are:
518518+519519+- [Trigger](#trigger): A **required** field that defines
520520+ when a workflow should be triggered.
521521+- [Engine](#engine): A **required** field that defines which
522522+ engine a workflow should run on.
523523+- [Clone options](#clone-options): An **optional** field
524524+ that defines how the repository should be cloned.
525525+- [Dependencies](#dependencies): An **optional** field that
526526+ allows you to list dependencies you may need.
527527+- [Environment](#environment): An **optional** field that
528528+ allows you to define environment variables.
529529+- [Steps](#steps): An **optional** field that allows you to
530530+ define what steps should run in the workflow.
531531+532532+### Trigger
533533+534534+The first thing to add to a workflow is the trigger, which
535535+defines when a workflow runs. This is defined using a `when`
536536+field, which takes in a list of conditions. Each condition
537537+has the following fields:
538538+539539+- `event`: This is a **required** field that defines when
540540+ your workflow should run. It's a list that can take one or
541541+ more of the following values:
542542+ - `push`: The workflow should run every time a commit is
543543+ pushed to the repository.
544544+ - `pull_request`: The workflow should run every time a
545545+ pull request is made or updated.
546546+ - `manual`: The workflow can be triggered manually.
547547+- `branch`: Defines which branches the workflow should run
548548+ for. If used with the `push` event, commits to the
549549+ branch(es) listed here will trigger the workflow. If used
550550+ with the `pull_request` event, updates to pull requests
551551+ targeting the branch(es) listed here will trigger the
552552+ workflow. This field has no effect with the `manual`
553553+ event. Supports glob patterns using `*` and `**` (e.g.,
554554+ `main`, `develop`, `release-*`). Either `branch` or `tag`
555555+ (or both) must be specified for `push` events.
556556+- `tag`: Defines which tags the workflow should run for.
557557+ Only used with the `push` event - when tags matching the
558558+ pattern(s) listed here are pushed, the workflow will
559559+ trigger. This field has no effect with `pull_request` or
560560+ `manual` events. Supports glob patterns using `*` and `**`
561561+ (e.g., `v*`, `v1.*`, `release-**`). Either `branch` or
562562+ `tag` (or both) must be specified for `push` events.
563563+564564+For example, if you'd like to define a workflow that runs
565565+when commits are pushed to the `main` and `develop`
566566+branches, or when pull requests that target the `main`
567567+branch are updated, or manually, you can do so with:
568568+569569+```yaml
570570+when:
571571+ - event: ["push", "manual"]
572572+ branch: ["main", "develop"]
573573+ - event: ["pull_request"]
574574+ branch: ["main"]
575575+```
576576+577577+You can also trigger workflows on tag pushes. For instance,
578578+to run a deployment workflow when tags matching `v*` are
579579+pushed:
580580+581581+```yaml
582582+when:
583583+ - event: ["push"]
584584+ tag: ["v*"]
585585+```
586586+587587+You can even combine branch and tag patterns in a single
588588+constraint (the workflow triggers if either matches):
589589+590590+```yaml
591591+when:
592592+ - event: ["push"]
593593+ branch: ["main", "release-*"]
594594+ tag: ["v*", "stable"]
595595+```
596596+597597+### Engine
598598+599599+Next is the engine on which the workflow should run, defined
600600+using the **required** `engine` field. The currently
601601+supported engines are:
602602+603603+- `nixery`: This uses an instance of
604604+ [Nixery](https://nixery.dev) to run steps, which allows
605605+ you to add [dependencies](#dependencies) from
606606+ [Nixpkgs](https://github.com/NixOS/nixpkgs). You can
607607+ search for packages on https://search.nixos.org, and
608608+ there's a pretty good chance the package(s) you're looking
609609+ for will be there.
610610+611611+Example:
612612+613613+```yaml
614614+engine: "nixery"
615615+```
616616+617617+### Clone options
618618+619619+When a workflow starts, the first step is to clone the
620620+repository. You can customize this behavior using the
621621+**optional** `clone` field. It has the following fields:
622622+623623+- `skip`: Setting this to `true` will skip cloning the
624624+ repository. This can be useful if your workflow is doing
625625+ something that doesn't require anything from the
626626+ repository itself. This is `false` by default.
627627+- `depth`: This sets the number of commits, or the "clone
628628+ depth", to fetch from the repository. For example, if you
629629+ set this to 2, the last 2 commits will be fetched. By
630630+ default, the depth is set to 1, meaning only the most
631631+ recent commit will be fetched, which is the commit that
632632+ triggered the workflow.
633633+- `submodules`: If you use [git
634634+ submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
635635+ in your repository, setting this field to `true` will
636636+ recursively fetch all submodules. This is `false` by
637637+ default.
638638+639639+The default settings are:
640640+641641+```yaml
642642+clone:
643643+ skip: false
644644+ depth: 1
645645+ submodules: false
646646+```
647647+648648+### Dependencies
649649+650650+Usually when you're running a workflow, you'll need
651651+additional dependencies. The `dependencies` field lets you
652652+define which dependencies to get, and from where. It's a
653653+key-value map, with the key being the registry to fetch
654654+dependencies from, and the value being the list of
655655+dependencies to fetch.
656656+657657+Say you want to fetch Node.js and Go from `nixpkgs`, and a
658658+package called `my_pkg` you've made from your own registry
659659+at your repository at
660660+`https://tangled.sh/@example.com/my_pkg`. You can define
661661+those dependencies like so:
662662+663663+```yaml
664664+dependencies:
665665+ # nixpkgs
666666+ nixpkgs:
667667+ - nodejs
668668+ - go
669669+ # custom registry
670670+ git+https://tangled.org/@example.com/my_pkg:
671671+ - my_pkg
672672+```
673673+674674+Now these dependencies are available to use in your
675675+workflow!
676676+677677+### Environment
678678+679679+The `environment` field allows you define environment
680680+variables that will be available throughout the entire
681681+workflow. **Do not put secrets here, these environment
682682+variables are visible to anyone viewing the repository. You
683683+can add secrets for pipelines in your repository's
684684+settings.**
685685+686686+Example:
687687+688688+```yaml
689689+environment:
690690+ GOOS: "linux"
691691+ GOARCH: "arm64"
692692+ NODE_ENV: "production"
693693+ MY_ENV_VAR: "MY_ENV_VALUE"
694694+```
695695+696696+### Steps
697697+698698+The `steps` field allows you to define what steps should run
699699+in the workflow. It's a list of step objects, each with the
700700+following fields:
701701+702702+- `name`: This field allows you to give your step a name.
703703+ This name is visible in your workflow runs, and is used to
704704+ describe what the step is doing.
705705+- `command`: This field allows you to define a command to
706706+ run in that step. The step is run in a Bash shell, and the
707707+ logs from the command will be visible in the pipelines
708708+ page on the Tangled website. The
709709+ [dependencies](#dependencies) you added will be available
710710+ to use here.
711711+- `environment`: Similar to the global
712712+ [environment](#environment) config, this **optional**
713713+ field is a key-value map that allows you to set
714714+ environment variables for the step. **Do not put secrets
715715+ here, these environment variables are visible to anyone
716716+ viewing the repository. You can add secrets for pipelines
717717+ in your repository's settings.**
718718+719719+Example:
720720+721721+```yaml
722722+steps:
723723+ - name: "Build backend"
724724+ command: "go build"
725725+ environment:
726726+ GOOS: "darwin"
727727+ GOARCH: "arm64"
728728+ - name: "Build frontend"
729729+ command: "npm run build"
730730+ environment:
731731+ NODE_ENV: "production"
732732+```
733733+734734+### Complete workflow
735735+736736+```yaml
737737+# .tangled/workflows/build.yml
738738+739739+when:
740740+ - event: ["push", "manual"]
741741+ branch: ["main", "develop"]
742742+ - event: ["pull_request"]
743743+ branch: ["main"]
744744+745745+engine: "nixery"
746746+747747+# using the default values
748748+clone:
749749+ skip: false
750750+ depth: 1
751751+ submodules: false
752752+753753+dependencies:
754754+ # nixpkgs
755755+ nixpkgs:
756756+ - nodejs
757757+ - go
758758+ # custom registry
759759+ git+https://tangled.org/@example.com/my_pkg:
760760+ - my_pkg
761761+762762+environment:
763763+ GOOS: "linux"
764764+ GOARCH: "arm64"
765765+ NODE_ENV: "production"
766766+ MY_ENV_VAR: "MY_ENV_VALUE"
767767+768768+steps:
769769+ - name: "Build backend"
770770+ command: "go build"
771771+ environment:
772772+ GOOS: "darwin"
773773+ GOARCH: "arm64"
774774+ - name: "Build frontend"
775775+ command: "npm run build"
776776+ environment:
777777+ NODE_ENV: "production"
778778+```
779779+780780+If you want another example of a workflow, you can look at
781781+the one [Tangled uses to build the
782782+project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
783783+784784+## Self-hosting guide
785785+786786+### Prerequisites
787787+788788+* Go
789789+* Docker (the only supported backend currently)
790790+791791+### Configuration
792792+793793+Spindle is configured using environment variables. The following environment variables are available:
794794+795795+* `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`).
796796+* `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`).
797797+* `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required).
798798+* `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`).
799799+* `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`).
800800+* `SPINDLE_SERVER_OWNER`: The DID of the owner (required).
801801+* `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`).
802802+* `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`).
803803+* `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`).
804804+805805+### Running spindle
806806+807807+1. **Set the environment variables.** For example:
808808+809809+ ```shell
810810+ export SPINDLE_SERVER_HOSTNAME="your-hostname"
811811+ export SPINDLE_SERVER_OWNER="your-did"
812812+ ```
813813+814814+2. **Build the Spindle binary.**
815815+816816+ ```shell
817817+ cd core
818818+ go mod download
819819+ go build -o cmd/spindle/spindle cmd/spindle/main.go
820820+ ```
821821+822822+3. **Create the log directory.**
823823+824824+ ```shell
825825+ sudo mkdir -p /var/log/spindle
826826+ sudo chown $USER:$USER -R /var/log/spindle
827827+ ```
828828+829829+4. **Run the Spindle binary.**
830830+831831+ ```shell
832832+ ./cmd/spindle/spindle
833833+ ```
834834+835835+Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
836836+837837+## Architecture
838838+839839+Spindle is a small CI runner service. Here's a high level overview of how it operates:
840840+841841+* listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
842842+[`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream.
843843+* when a new repo record comes through (typically when you add a spindle to a
844844+repo from the settings), spindle then resolves the underlying knot and
845845+subscribes to repo events (see:
846846+[`sh.tangled.pipeline`](/lexicons/pipeline.json)).
847847+* the spindle engine then handles execution of the pipeline, with results and
848848+logs beamed on the spindle event stream over wss
849849+850850+### The engine
851851+852852+At present, the only supported backend is Docker (and Podman, if Docker
853853+compatibility is enabled, so that `/run/docker.sock` is created). Spindle
854854+executes each step in the pipeline in a fresh container, with state persisted
855855+across steps within the `/tangled/workspace` directory.
856856+857857+The base image for the container is constructed on the fly using
858858+[Nixery](https://nixery.dev), which is handy for caching layers for frequently
859859+used packages.
860860+861861+The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
862862+863863+## Secrets with openbao
864864+865865+This document covers setting up Spindle to use OpenBao for secrets
866866+management via OpenBao Proxy instead of the default SQLite backend.
867867+868868+### Overview
869869+870870+Spindle now uses OpenBao Proxy for secrets management. The proxy handles
871871+authentication automatically using AppRole credentials, while Spindle
872872+connects to the local proxy instead of directly to the OpenBao server.
873873+874874+This approach provides better security, automatic token renewal, and
875875+simplified application code.
876876+877877+### Installation
878878+879879+Install OpenBao from nixpkgs:
880880+881881+```bash
882882+nix shell nixpkgs#openbao # for a local server
883883+```
884884+885885+### Setup
886886+887887+The setup process can is documented for both local development and production.
888888+889889+#### Local development
890890+891891+Start OpenBao in dev mode:
892892+893893+```bash
894894+bao server -dev -dev-root-token-id="root" -dev-listen-address=127.0.0.1:8201
895895+```
896896+897897+This starts OpenBao on `http://localhost:8201` with a root token.
898898+899899+Set up environment for bao CLI:
900900+901901+```bash
902902+export BAO_ADDR=http://localhost:8200
903903+export BAO_TOKEN=root
904904+```
905905+906906+#### Production
907907+908908+You would typically use a systemd service with a
909909+configuration file. Refer to
910910+[@tangled.org/infra](https://tangled.org/@tangled.org/infra)
911911+for how this can be achieved using Nix.
912912+913913+Then, initialize the bao server:
914914+915915+```bash
916916+bao operator init -key-shares=1 -key-threshold=1
917917+```
918918+919919+This will print out an unseal key and a root key. Save them
920920+somewhere (like a password manager). Then unseal the vault
921921+to begin setting it up:
922922+923923+```bash
924924+bao operator unseal <unseal_key>
925925+```
926926+927927+All steps below remain the same across both dev and
928928+production setups.
929929+930930+#### Configure openbao server
931931+932932+Create the spindle KV mount:
933933+934934+```bash
935935+bao secrets enable -path=spindle -version=2 kv
936936+```
937937+938938+Set up AppRole authentication and policy:
939939+940940+Create a policy file `spindle-policy.hcl`:
941941+942942+```hcl
943943+# Full access to spindle KV v2 data
944944+path "spindle/data/*" {
945945+ capabilities = ["create", "read", "update", "delete"]
946946+}
947947+948948+# Access to metadata for listing and management
949949+path "spindle/metadata/*" {
950950+ capabilities = ["list", "read", "delete", "update"]
951951+}
952952+953953+# Allow listing at root level
954954+path "spindle/" {
955955+ capabilities = ["list"]
956956+}
957957+958958+# Required for connection testing and health checks
959959+path "auth/token/lookup-self" {
960960+ capabilities = ["read"]
961961+}
962962+```
963963+964964+Apply the policy and create an AppRole:
965965+966966+```bash
967967+bao policy write spindle-policy spindle-policy.hcl
968968+bao auth enable approle
969969+bao write auth/approle/role/spindle \
970970+ token_policies="spindle-policy" \
971971+ token_ttl=1h \
972972+ token_max_ttl=4h \
973973+ bind_secret_id=true \
974974+ secret_id_ttl=0 \
975975+ secret_id_num_uses=0
976976+```
977977+978978+Get the credentials:
979979+980980+```bash
981981+# Get role ID (static)
982982+ROLE_ID=$(bao read -field=role_id auth/approle/role/spindle/role-id)
983983+984984+# Generate secret ID
985985+SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/spindle/secret-id)
986986+987987+echo "Role ID: $ROLE_ID"
988988+echo "Secret ID: $SECRET_ID"
989989+```
990990+991991+#### Create proxy configuration
992992+993993+Create the credential files:
994994+995995+```bash
996996+# Create directory for OpenBao files
997997+mkdir -p /tmp/openbao
998998+999999+# Save credentials
10001000+echo "$ROLE_ID" > /tmp/openbao/role-id
10011001+echo "$SECRET_ID" > /tmp/openbao/secret-id
10021002+chmod 600 /tmp/openbao/role-id /tmp/openbao/secret-id
10031003+```
10041004+10051005+Create a proxy configuration file `/tmp/openbao/proxy.hcl`:
10061006+10071007+```hcl
10081008+# OpenBao server connection
10091009+vault {
10101010+ address = "http://localhost:8200"
10111011+}
10121012+10131013+# Auto-Auth using AppRole
10141014+auto_auth {
10151015+ method "approle" {
10161016+ mount_path = "auth/approle"
10171017+ config = {
10181018+ role_id_file_path = "/tmp/openbao/role-id"
10191019+ secret_id_file_path = "/tmp/openbao/secret-id"
10201020+ }
10211021+ }
10221022+10231023+ # Optional: write token to file for debugging
10241024+ sink "file" {
10251025+ config = {
10261026+ path = "/tmp/openbao/token"
10271027+ mode = 0640
10281028+ }
10291029+ }
10301030+}
10311031+10321032+# Proxy listener for Spindle
10331033+listener "tcp" {
10341034+ address = "127.0.0.1:8201"
10351035+ tls_disable = true
10361036+}
10371037+10381038+# Enable API proxy with auto-auth token
10391039+api_proxy {
10401040+ use_auto_auth_token = true
10411041+}
10421042+10431043+# Enable response caching
10441044+cache {
10451045+ use_auto_auth_token = true
10461046+}
10471047+10481048+# Logging
10491049+log_level = "info"
10501050+```
10511051+10521052+#### Start the proxy
10531053+10541054+Start OpenBao Proxy:
10551055+10561056+```bash
10571057+bao proxy -config=/tmp/openbao/proxy.hcl
10581058+```
10591059+10601060+The proxy will authenticate with OpenBao and start listening on
10611061+`127.0.0.1:8201`.
10621062+10631063+#### Configure spindle
10641064+10651065+Set these environment variables for Spindle:
10661066+10671067+```bash
10681068+export SPINDLE_SERVER_SECRETS_PROVIDER=openbao
10691069+export SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=http://127.0.0.1:8201
10701070+export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle
10711071+```
10721072+10731073+On startup, the spindle will now connect to the local proxy,
10741074+which handles all authentication automatically.
10751075+10761076+### Production setup for proxy
10771077+10781078+For production, you'll want to run the proxy as a service:
10791079+10801080+Place your production configuration in
10811081+`/etc/openbao/proxy.hcl` with proper TLS settings for the
10821082+vault connection.
10831083+10841084+### Verifying setup
10851085+10861086+Test the proxy directly:
10871087+10881088+```bash
10891089+# Check proxy health
10901090+curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/sys/health
10911091+10921092+# Test token lookup through proxy
10931093+curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/auth/token/lookup-self
10941094+```
10951095+10961096+Test OpenBao operations through the server:
10971097+10981098+```bash
10991099+# List all secrets
11001100+bao kv list spindle/
11011101+11021102+# Add a test secret via Spindle API, then check it exists
11031103+bao kv list spindle/repos/
11041104+11051105+# Get a specific secret
11061106+bao kv get spindle/repos/your_repo_path/SECRET_NAME
11071107+```
11081108+11091109+### How it works
11101110+11111111+- Spindle connects to OpenBao Proxy on localhost (typically
11121112+ port 8200 or 8201)
11131113+- The proxy authenticates with OpenBao using AppRole
11141114+ credentials
11151115+- All Spindle requests go through the proxy, which injects
11161116+ authentication tokens
11171117+- Secrets are stored at
11181118+ `spindle/repos/{sanitized_repo_path}/{secret_key}`
11191119+- Repository paths like `did:plc:alice/myrepo` become
11201120+ `did_plc_alice_myrepo`
11211121+- The proxy handles all token renewal automatically
11221122+- Spindle no longer manages tokens or authentication
11231123+ directly
11241124+11251125+### Troubleshooting
11261126+11271127+**Connection refused**: Check that the OpenBao Proxy is
11281128+running and listening on the configured address.
11291129+11301130+**403 errors**: Verify the AppRole credentials are correct
11311131+and the policy has the necessary permissions.
11321132+11331133+**404 route errors**: The spindle KV mount probably doesn't
11341134+exist - run the mount creation step again.
11351135+11361136+**Proxy authentication failures**: Check the proxy logs and
11371137+verify the role-id and secret-id files are readable and
11381138+contain valid credentials.
11391139+11401140+**Secret not found after writing**: This can indicate policy
11411141+permission issues. Verify the policy includes both
11421142+`spindle/data/*` and `spindle/metadata/*` paths with
11431143+appropriate capabilities.
11441144+11451145+Check proxy logs:
11461146+11471147+```bash
11481148+# If running as systemd service
11491149+journalctl -u openbao-proxy -f
11501150+11511151+# If running directly, check the console output
11521152+```
11531153+11541154+Test AppRole authentication manually:
11551155+11561156+```bash
11571157+bao write auth/approle/login \
11581158+ role_id="$(cat /tmp/openbao/role-id)" \
11591159+ secret_id="$(cat /tmp/openbao/secret-id)"
11601160+```
11611161+11621162+# Migrating knots & spindles
11631163+11641164+Sometimes, non-backwards compatible changes are made to the
11651165+knot/spindle XRPC APIs. If you host a knot or a spindle, you
11661166+will need to follow this guide to upgrade. Typically, this
11671167+only requires you to deploy the newest version.
11681168+11691169+This document is laid out in reverse-chronological order.
11701170+Newer migration guides are listed first, and older guides
11711171+are further down the page.
11721172+11731173+## Upgrading from v1.8.x
11741174+11751175+After v1.8.2, the HTTP API for knot and spindles have been
11761176+deprecated and replaced with XRPC. Repositories on outdated
11771177+knots will not be viewable from the appview. Upgrading is
11781178+straightforward however.
11791179+11801180+For knots:
11811181+11821182+- Upgrade to latest tag (v1.9.0 or above)
11831183+- Head to the [knot dashboard](https://tangled.org/settings/knots) and
11841184+ hit the "retry" button to verify your knot
11851185+11861186+For spindles:
11871187+11881188+- Upgrade to latest tag (v1.9.0 or above)
11891189+- Head to the [spindle
11901190+ dashboard](https://tangled.org/settings/spindles) and hit the
11911191+ "retry" button to verify your spindle
11921192+11931193+## Upgrading from v1.7.x
11941194+11951195+After v1.7.0, knot secrets have been deprecated. You no
11961196+longer need a secret from the appview to run a knot. All
11971197+authorized commands to knots are managed via [Inter-Service
11981198+Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
11991199+Knots will be read-only until upgraded.
12001200+12011201+Upgrading is quite easy, in essence:
12021202+12031203+- `KNOT_SERVER_SECRET` is no more, you can remove this
12041204+ environment variable entirely
12051205+- `KNOT_SERVER_OWNER` is now required on boot, set this to
12061206+ your DID. You can find your DID in the
12071207+ [settings](https://tangled.org/settings) page.
12081208+- Restart your knot once you have replaced the environment
12091209+ variable
12101210+- Head to the [knot dashboard](https://tangled.org/settings/knots) and
12111211+ hit the "retry" button to verify your knot. This simply
12121212+ writes a `sh.tangled.knot` record to your PDS.
12131213+12141214+If you use the nix module, simply bump the flake to the
12151215+latest revision, and change your config block like so:
12161216+12171217+```diff
12181218+ services.tangled.knot = {
12191219+ enable = true;
12201220+ server = {
12211221+- secretFile = /path/to/secret;
12221222++ owner = "did:plc:foo";
12231223+ };
12241224+ };
12251225+```
12261226+12271227+# Hacking on Tangled
12281228+12291229+We highly recommend [installing
12301230+nix](https://nixos.org/download/) (the package manager)
12311231+before working on the codebase. The nix flake provides a lot
12321232+of helpers to get started and most importantly, builds and
12331233+dev shells are entirely deterministic.
12341234+12351235+To set up your dev environment:
12361236+12371237+```bash
12381238+nix develop
12391239+```
12401240+12411241+Non-nix users can look at the `devShell` attribute in the
12421242+`flake.nix` file to determine necessary dependencies.
12431243+12441244+## Running the appview
12451245+12461246+The nix flake also exposes a few `app` attributes (run `nix
12471247+flake show` to see a full list of what the flake provides),
12481248+one of the apps runs the appview with the `air`
12491249+live-reloader:
12501250+12511251+```bash
12521252+TANGLED_DEV=true nix run .#watch-appview
12531253+12541254+# TANGLED_DB_PATH might be of interest to point to
12551255+# different sqlite DBs
12561256+12571257+# in a separate shell, you can live-reload tailwind
12581258+nix run .#watch-tailwind
12591259+```
12601260+12611261+To authenticate with the appview, you will need redis and
12621262+OAUTH JWKs to be setup:
12631263+12641264+```
12651265+# oauth jwks should already be setup by the nix devshell:
12661266+echo $TANGLED_OAUTH_CLIENT_SECRET
12671267+z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc
12681268+12691269+echo $TANGLED_OAUTH_CLIENT_KID
12701270+1761667908
12711271+12721272+# if not, you can set it up yourself:
12731273+goat key generate -t P-256
12741274+Key Type: P-256 / secp256r1 / ES256 private key
12751275+Secret Key (Multibase Syntax): save this securely (eg, add to password manager)
12761276+ z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL
12771277+Public Key (DID Key Syntax): share or publish this (eg, in DID document)
12781278+ did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR
12791279+12801280+# the secret key from above
12811281+export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..."
12821282+12831283+# run redis in at a new shell to store oauth sessions
12841284+redis-server
12851285+```
12861286+12871287+## Running knots and spindles
12881288+12891289+An end-to-end knot setup requires setting up a machine with
12901290+`sshd`, `AuthorizedKeysCommand`, and git user, which is
12911291+quite cumbersome. So the nix flake provides a
12921292+`nixosConfiguration` to do so.
12931293+12941294+<details>
12951295+ <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
12961296+12971297+ In order to build Tangled's dev VM on macOS, you will
12981298+ first need to set up a Linux Nix builder. The recommended
12991299+ way to do so is to run a [`darwin.linux-builder`
13001300+ VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder)
13011301+ and to register it in `nix.conf` as a builder for Linux
13021302+ with the same architecture as your Mac (`linux-aarch64` if
13031303+ you are using Apple Silicon).
13041304+13051305+ > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
13061306+ > the tangled repo so that it doesn't conflict with the other VM. For example,
13071307+ > you can do
13081308+ >
13091309+ > ```shell
13101310+ > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder
13111311+ > ```
13121312+ >
13131313+ > to store the builder VM in a temporary dir.
13141314+ >
13151315+ > You should read and follow [all the other intructions][darwin builder vm] to
13161316+ > avoid subtle problems.
13171317+13181318+ Alternatively, you can use any other method to set up a
13191319+ Linux machine with `nix` installed that you can `sudo ssh`
13201320+ into (in other words, root user on your Mac has to be able
13211321+ to ssh into the Linux machine without entering a password)
13221322+ and that has the same architecture as your Mac. See
13231323+ [remote builder
13241324+ instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements)
13251325+ for how to register such a builder in `nix.conf`.
13261326+13271327+ > WARNING: If you'd like to use
13281328+ > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or
13291329+ > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo
13301330+ > ssh` works can be tricky. It seems to be [possible with
13311331+ > Orbstack](https://github.com/orgs/orbstack/discussions/1669).
13321332+13331333+</details>
13341334+13351335+To begin, grab your DID from http://localhost:3000/settings.
13361336+Then, set `TANGLED_VM_KNOT_OWNER` and
13371337+`TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a
13381338+lightweight NixOS VM like so:
13391339+13401340+```bash
13411341+nix run --impure .#vm
13421342+13431343+# type `poweroff` at the shell to exit the VM
13441344+```
13451345+13461346+This starts a knot on port 6444, a spindle on port 6555
13471347+with `ssh` exposed on port 2222.
13481348+13491349+Once the services are running, head to
13501350+http://localhost:3000/settings/knots and hit verify. It should
13511351+verify the ownership of the services instantly if everything
13521352+went smoothly.
13531353+13541354+You can push repositories to this VM with this ssh config
13551355+block on your main machine:
13561356+13571357+```bash
13581358+Host nixos-shell
13591359+ Hostname localhost
13601360+ Port 2222
13611361+ User git
13621362+ IdentityFile ~/.ssh/my_tangled_key
13631363+```
13641364+13651365+Set up a remote called `local-dev` on a git repo:
13661366+13671367+```bash
13681368+git remote add local-dev git@nixos-shell:user/repo
13691369+git push local-dev main
13701370+```
13711371+13721372+The above VM should already be running a spindle on
13731373+`localhost:6555`. Head to http://localhost:3000/settings/spindles and
13741374+hit verify. You can then configure each repository to use
13751375+this spindle and run CI jobs.
13761376+13771377+Of interest when debugging spindles:
13781378+13791379+```
13801380+# service logs from journald:
13811381+journalctl -xeu spindle
13821382+13831383+# CI job logs from disk:
13841384+ls /var/log/spindle
13851385+13861386+# debugging spindle db:
13871387+sqlite3 /var/lib/spindle/spindle.db
13881388+13891389+# litecli has a nicer REPL interface:
13901390+litecli /var/lib/spindle/spindle.db
13911391+```
13921392+13931393+If for any reason you wish to disable either one of the
13941394+services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set
13951395+`services.tangled.spindle.enable` (or
13961396+`services.tangled.knot.enable`) to `false`.
13971397+13981398+# Contribution guide
13991399+14001400+## Commit guidelines
14011401+14021402+We follow a commit style similar to the Go project. Please keep commits:
14031403+14041404+* **atomic**: each commit should represent one logical change
14051405+* **descriptive**: the commit message should clearly describe what the
14061406+change does and why it's needed
14071407+14081408+### Message format
14091409+14101410+```
14111411+<service/top-level directory>/<affected package/directory>: <short summary of change>
14121412+14131413+Optional longer description can go here, if necessary. Explain what the
14141414+change does and why, especially if not obvious. Reference relevant
14151415+issues or PRs when applicable. These can be links for now since we don't
14161416+auto-link issues/PRs yet.
14171417+```
14181418+14191419+Here are some examples:
14201420+14211421+```
14221422+appview/state: fix token expiry check in middleware
14231423+14241424+The previous check did not account for clock drift, leading to premature
14251425+token invalidation.
14261426+```
14271427+14281428+```
14291429+knotserver/git/service: improve error checking in upload-pack
14301430+```
14311431+14321432+14331433+### General notes
14341434+14351435+- PRs get merged "as-is" (fast-forward) -- like applying a patch-series
14361436+using `git am`. At present, there is no squashing -- so please author
14371437+your commits as they would appear on `master`, following the above
14381438+guidelines.
14391439+- If there is a lot of nesting, for example "appview:
14401440+pages/templates/repo/fragments: ...", these can be truncated down to
14411441+just "appview: repo/fragments: ...". If the change affects a lot of
14421442+subdirectories, you may abbreviate to just the top-level names, e.g.
14431443+"appview: ..." or "knotserver: ...".
14441444+- Keep commits lowercased with no trailing period.
14451445+- Use the imperative mood in the summary line (e.g., "fix bug" not
14461446+"fixed bug" or "fixes bug").
14471447+- Try to keep the summary line under 72 characters, but we aren't too
14481448+fussed about this.
14491449+- Follow the same formatting for PR titles if filled manually.
14501450+- Don't include unrelated changes in the same commit.
14511451+- Avoid noisy commit messages like "wip" or "final fix"—rewrite history
14521452+before submitting if necessary.
14531453+14541454+## Code formatting
14551455+14561456+We use a variety of tools to format our code, and multiplex them with
14571457+[`treefmt`](https://treefmt.com): all you need to do to format your changes
14581458+is run `nix run .#fmt` (or just `treefmt` if you're in the devshell).
14591459+14601460+## Proposals for bigger changes
14611461+14621462+Small fixes like typos, minor bugs, or trivial refactors can be
14631463+submitted directly as PRs.
14641464+14651465+For larger changes—especially those introducing new features, significant
14661466+refactoring, or altering system behavior—please open a proposal first. This
14671467+helps us evaluate the scope, design, and potential impact before implementation.
14681468+14691469+Create a new issue titled:
14701470+14711471+```
14721472+proposal: <affected scope>: <summary of change>
14731473+```
14741474+14751475+In the description, explain:
14761476+14771477+- What the change is
14781478+- Why it's needed
14791479+- How you plan to implement it (roughly)
14801480+- Any open questions or tradeoffs
14811481+14821482+We'll use the issue thread to discuss and refine the idea before moving
14831483+forward.
14841484+14851485+## Developer certificate of origin (DCO)
14861486+14871487+We require all contributors to certify that they have the right to
14881488+submit the code they're contributing. To do this, we follow the
14891489+[Developer Certificate of Origin
14901490+(DCO)](https://developercertificate.org/).
14911491+14921492+By signing your commits, you're stating that the contribution is your
14931493+own work, or that you have the right to submit it under the project's
14941494+license. This helps us keep things clean and legally sound.
14951495+14961496+To sign your commit, just add the `-s` flag when committing:
14971497+14981498+```sh
14991499+git commit -s -m "your commit message"
15001500+```
15011501+15021502+This appends a line like:
15031503+15041504+```
15051505+Signed-off-by: Your Name <your.email@example.com>
15061506+```
15071507+15081508+We won't merge commits if they aren't signed off. If you forget, you can
15091509+amend the last commit like this:
15101510+15111511+```sh
15121512+git commit --amend -s
15131513+```
15141514+15151515+If you're submitting a PR with multiple commits, make sure each one is
15161516+signed.
15171517+15181518+For [jj](https://jj-vcs.github.io/jj/latest/) users, you can run the following command
15191519+to make it sign off commits in the tangled repo:
15201520+15211521+```shell
15221522+# Safety check, should say "No matching config key..."
15231523+jj config list templates.commit_trailers
15241524+# The command below may need to be adjusted if the command above returned something.
15251525+jj config set --repo templates.commit_trailers "format_signed_off_by_trailer(self)"
15261526+```
15271527+15281528+Refer to the [jujutsu
15291529+documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers)
15301530+for more information.
-136
docs/contributing.md
···11-# tangled contributing guide
22-33-## commit guidelines
44-55-We follow a commit style similar to the Go project. Please keep commits:
66-77-* **atomic**: each commit should represent one logical change
88-* **descriptive**: the commit message should clearly describe what the
99-change does and why it's needed
1010-1111-### message format
1212-1313-```
1414-<service/top-level directory>/<affected package/directory>: <short summary of change>
1515-1616-1717-Optional longer description can go here, if necessary. Explain what the
1818-change does and why, especially if not obvious. Reference relevant
1919-issues or PRs when applicable. These can be links for now since we don't
2020-auto-link issues/PRs yet.
2121-```
2222-2323-Here are some examples:
2424-2525-```
2626-appview/state: fix token expiry check in middleware
2727-2828-The previous check did not account for clock drift, leading to premature
2929-token invalidation.
3030-```
3131-3232-```
3333-knotserver/git/service: improve error checking in upload-pack
3434-```
3535-3636-3737-### general notes
3838-3939-- PRs get merged "as-is" (fast-forward) -- like applying a patch-series
4040-using `git am`. At present, there is no squashing -- so please author
4141-your commits as they would appear on `master`, following the above
4242-guidelines.
4343-- If there is a lot of nesting, for example "appview:
4444-pages/templates/repo/fragments: ...", these can be truncated down to
4545-just "appview: repo/fragments: ...". If the change affects a lot of
4646-subdirectories, you may abbreviate to just the top-level names, e.g.
4747-"appview: ..." or "knotserver: ...".
4848-- Keep commits lowercased with no trailing period.
4949-- Use the imperative mood in the summary line (e.g., "fix bug" not
5050-"fixed bug" or "fixes bug").
5151-- Try to keep the summary line under 72 characters, but we aren't too
5252-fussed about this.
5353-- Follow the same formatting for PR titles if filled manually.
5454-- Don't include unrelated changes in the same commit.
5555-- Avoid noisy commit messages like "wip" or "final fix"—rewrite history
5656-before submitting if necessary.
5757-5858-## code formatting
5959-6060-We use a variety of tools to format our code, and multiplex them with
6161-[`treefmt`](https://treefmt.com): all you need to do to format your changes
6262-is run `nix run .#fmt` (or just `treefmt` if you're in the devshell).
6363-6464-## proposals for bigger changes
6565-6666-Small fixes like typos, minor bugs, or trivial refactors can be
6767-submitted directly as PRs.
6868-6969-For larger changes—especially those introducing new features, significant
7070-refactoring, or altering system behavior—please open a proposal first. This
7171-helps us evaluate the scope, design, and potential impact before implementation.
7272-7373-### proposal format
7474-7575-Create a new issue titled:
7676-7777-```
7878-proposal: <affected scope>: <summary of change>
7979-```
8080-8181-In the description, explain:
8282-8383-- What the change is
8484-- Why it's needed
8585-- How you plan to implement it (roughly)
8686-- Any open questions or tradeoffs
8787-8888-We'll use the issue thread to discuss and refine the idea before moving
8989-forward.
9090-9191-## developer certificate of origin (DCO)
9292-9393-We require all contributors to certify that they have the right to
9494-submit the code they're contributing. To do this, we follow the
9595-[Developer Certificate of Origin
9696-(DCO)](https://developercertificate.org/).
9797-9898-By signing your commits, you're stating that the contribution is your
9999-own work, or that you have the right to submit it under the project's
100100-license. This helps us keep things clean and legally sound.
101101-102102-To sign your commit, just add the `-s` flag when committing:
103103-104104-```sh
105105-git commit -s -m "your commit message"
106106-```
107107-108108-This appends a line like:
109109-110110-```
111111-Signed-off-by: Your Name <your.email@example.com>
112112-```
113113-114114-We won't merge commits if they aren't signed off. If you forget, you can
115115-amend the last commit like this:
116116-117117-```sh
118118-git commit --amend -s
119119-```
120120-121121-If you're submitting a PR with multiple commits, make sure each one is
122122-signed.
123123-124124-For [jj](https://jj-vcs.github.io/jj/latest/) users, you can run the following command
125125-to make it sign off commits in the tangled repo:
126126-127127-```shell
128128-# Safety check, should say "No matching config key..."
129129-jj config list templates.commit_trailers
130130-# The command below may need to be adjusted if the command above returned something.
131131-jj config set --repo templates.commit_trailers "format_signed_off_by_trailer(self)"
132132-```
133133-134134-Refer to the [jj
135135-documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers)
136136-for more information.
-172
docs/hacking.md
···11-# hacking on tangled
22-33-We highly recommend [installing
44-nix](https://nixos.org/download/) (the package manager)
55-before working on the codebase. The nix flake provides a lot
66-of helpers to get started and most importantly, builds and
77-dev shells are entirely deterministic.
88-99-To set up your dev environment:
1010-1111-```bash
1212-nix develop
1313-```
1414-1515-Non-nix users can look at the `devShell` attribute in the
1616-`flake.nix` file to determine necessary dependencies.
1717-1818-## running the appview
1919-2020-The nix flake also exposes a few `app` attributes (run `nix
2121-flake show` to see a full list of what the flake provides),
2222-one of the apps runs the appview with the `air`
2323-live-reloader:
2424-2525-```bash
2626-TANGLED_DEV=true nix run .#watch-appview
2727-2828-# TANGLED_DB_PATH might be of interest to point to
2929-# different sqlite DBs
3030-3131-# in a separate shell, you can live-reload tailwind
3232-nix run .#watch-tailwind
3333-```
3434-3535-To authenticate with the appview, you will need redis and
3636-OAUTH JWKs to be setup:
3737-3838-```
3939-# oauth jwks should already be setup by the nix devshell:
4040-echo $TANGLED_OAUTH_CLIENT_SECRET
4141-z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc
4242-4343-echo $TANGLED_OAUTH_CLIENT_KID
4444-1761667908
4545-4646-# if not, you can set it up yourself:
4747-goat key generate -t P-256
4848-Key Type: P-256 / secp256r1 / ES256 private key
4949-Secret Key (Multibase Syntax): save this securely (eg, add to password manager)
5050- z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL
5151-Public Key (DID Key Syntax): share or publish this (eg, in DID document)
5252- did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR
5353-5454-# the secret key from above
5555-export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..."
5656-5757-# run redis in at a new shell to store oauth sessions
5858-redis-server
5959-```
6060-6161-## running knots and spindles
6262-6363-An end-to-end knot setup requires setting up a machine with
6464-`sshd`, `AuthorizedKeysCommand`, and git user, which is
6565-quite cumbersome. So the nix flake provides a
6666-`nixosConfiguration` to do so.
6767-6868-<details>
6969- <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
7070-7171- In order to build Tangled's dev VM on macOS, you will
7272- first need to set up a Linux Nix builder. The recommended
7373- way to do so is to run a [`darwin.linux-builder`
7474- VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder)
7575- and to register it in `nix.conf` as a builder for Linux
7676- with the same architecture as your Mac (`linux-aarch64` if
7777- you are using Apple Silicon).
7878-7979- > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
8080- > the tangled repo so that it doesn't conflict with the other VM. For example,
8181- > you can do
8282- >
8383- > ```shell
8484- > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder
8585- > ```
8686- >
8787- > to store the builder VM in a temporary dir.
8888- >
8989- > You should read and follow [all the other intructions][darwin builder vm] to
9090- > avoid subtle problems.
9191-9292- Alternatively, you can use any other method to set up a
9393- Linux machine with `nix` installed that you can `sudo ssh`
9494- into (in other words, root user on your Mac has to be able
9595- to ssh into the Linux machine without entering a password)
9696- and that has the same architecture as your Mac. See
9797- [remote builder
9898- instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements)
9999- for how to register such a builder in `nix.conf`.
100100-101101- > WARNING: If you'd like to use
102102- > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or
103103- > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo
104104- > ssh` works can be tricky. It seems to be [possible with
105105- > Orbstack](https://github.com/orgs/orbstack/discussions/1669).
106106-107107-</details>
108108-109109-To begin, grab your DID from http://localhost:3000/settings.
110110-Then, set `TANGLED_VM_KNOT_OWNER` and
111111-`TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a
112112-lightweight NixOS VM like so:
113113-114114-```bash
115115-nix run --impure .#vm
116116-117117-# type `poweroff` at the shell to exit the VM
118118-```
119119-120120-This starts a knot on port 6444, a spindle on port 6555
121121-with `ssh` exposed on port 2222.
122122-123123-Once the services are running, head to
124124-http://localhost:3000/settings/knots and hit verify. It should
125125-verify the ownership of the services instantly if everything
126126-went smoothly.
127127-128128-You can push repositories to this VM with this ssh config
129129-block on your main machine:
130130-131131-```bash
132132-Host nixos-shell
133133- Hostname localhost
134134- Port 2222
135135- User git
136136- IdentityFile ~/.ssh/my_tangled_key
137137-```
138138-139139-Set up a remote called `local-dev` on a git repo:
140140-141141-```bash
142142-git remote add local-dev git@nixos-shell:user/repo
143143-git push local-dev main
144144-```
145145-146146-### running a spindle
147147-148148-The above VM should already be running a spindle on
149149-`localhost:6555`. Head to http://localhost:3000/settings/spindles and
150150-hit verify. You can then configure each repository to use
151151-this spindle and run CI jobs.
152152-153153-Of interest when debugging spindles:
154154-155155-```
156156-# service logs from journald:
157157-journalctl -xeu spindle
158158-159159-# CI job logs from disk:
160160-ls /var/log/spindle
161161-162162-# debugging spindle db:
163163-sqlite3 /var/lib/spindle/spindle.db
164164-165165-# litecli has a nicer REPL interface:
166166-litecli /var/lib/spindle/spindle.db
167167-```
168168-169169-If for any reason you wish to disable either one of the
170170-services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set
171171-`services.tangled.spindle.enable` (or
172172-`services.tangled.knot.enable`) to `false`.
···11-# knot self-hosting guide
22-33-So you want to run your own knot server? Great! Here are a few prerequisites:
44-55-1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind.
66-2. A (sub)domain name. People generally use `knot.example.com`.
77-3. A valid SSL certificate for your domain.
88-99-There's a couple of ways to get started:
1010-* NixOS: refer to
1111-[flake.nix](https://tangled.sh/@tangled.sh/core/blob/master/flake.nix)
1212-* Docker: Documented at
1313-[@tangled.sh/knot-docker](https://tangled.sh/@tangled.sh/knot-docker)
1414-(community maintained: support is not guaranteed!)
1515-* Manual: Documented below.
1616-1717-## manual setup
1818-1919-First, clone this repository:
2020-2121-```
2222-git clone https://tangled.org/@tangled.org/core
2323-```
2424-2525-Then, build the `knot` CLI. This is the knot administration and operation tool.
2626-For the purpose of this guide, we're only concerned with these subcommands:
2727-2828-* `knot server`: the main knot server process, typically run as a
2929-supervised service
3030-* `knot guard`: handles role-based access control for git over SSH
3131-(you'll never have to run this yourself)
3232-* `knot keys`: fetches SSH keys associated with your knot; we'll use
3333-this to generate the SSH `AuthorizedKeysCommand`
3434-3535-```
3636-cd core
3737-export CGO_ENABLED=1
3838-go build -o knot ./cmd/knot
3939-```
4040-4141-Next, move the `knot` binary to a location owned by `root` --
4242-`/usr/local/bin/` is a good choice. Make sure the binary itself is also owned by `root`:
4343-4444-```
4545-sudo mv knot /usr/local/bin/knot
4646-sudo chown root:root /usr/local/bin/knot
4747-```
4848-4949-This is necessary because SSH `AuthorizedKeysCommand` requires [really
5050-specific permissions](https://stackoverflow.com/a/27638306). The
5151-`AuthorizedKeysCommand` specifies a command that is run by `sshd` to
5252-retrieve a user's public SSH keys dynamically for authentication. Let's
5353-set that up.
5454-5555-```
5656-sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
5757-Match User git
5858- AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys
5959- AuthorizedKeysCommandUser nobody
6060-EOF
6161-```
6262-6363-Then, reload `sshd`:
6464-6565-```
6666-sudo systemctl reload ssh
6767-```
6868-6969-Next, create the `git` user. We'll use the `git` user's home directory
7070-to store repositories:
7171-7272-```
7373-sudo adduser git
7474-```
7575-7676-Create `/home/git/.knot.env` with the following, updating the values as
7777-necessary. The `KNOT_SERVER_OWNER` should be set to your
7878-DID, you can find your DID in the [Settings](https://tangled.sh/settings) page.
7979-8080-```
8181-KNOT_REPO_SCAN_PATH=/home/git
8282-KNOT_SERVER_HOSTNAME=knot.example.com
8383-APPVIEW_ENDPOINT=https://tangled.sh
8484-KNOT_SERVER_OWNER=did:plc:foobar
8585-KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
8686-KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
8787-```
8888-8989-If you run a Linux distribution that uses systemd, you can use the provided
9090-service file to run the server. Copy
9191-[`knotserver.service`](/systemd/knotserver.service)
9292-to `/etc/systemd/system/`. Then, run:
9393-9494-```
9595-systemctl enable knotserver
9696-systemctl start knotserver
9797-```
9898-9999-The last step is to configure a reverse proxy like Nginx or Caddy to front your
100100-knot. Here's an example configuration for Nginx:
101101-102102-```
103103-server {
104104- listen 80;
105105- listen [::]:80;
106106- server_name knot.example.com;
107107-108108- location / {
109109- proxy_pass http://localhost:5555;
110110- proxy_set_header Host $host;
111111- proxy_set_header X-Real-IP $remote_addr;
112112- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
113113- proxy_set_header X-Forwarded-Proto $scheme;
114114- }
115115-116116- # wss endpoint for git events
117117- location /events {
118118- proxy_set_header X-Forwarded-For $remote_addr;
119119- proxy_set_header Host $http_host;
120120- proxy_set_header Upgrade websocket;
121121- proxy_set_header Connection Upgrade;
122122- proxy_pass http://localhost:5555;
123123- }
124124- # additional config for SSL/TLS go here.
125125-}
126126-127127-```
128128-129129-Remember to use Let's Encrypt or similar to procure a certificate for your
130130-knot domain.
131131-132132-You should now have a running knot server! You can finalize
133133-your registration by hitting the `verify` button on the
134134-[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
135135-a record on your PDS to announce the existence of the knot.
136136-137137-### custom paths
138138-139139-(This section applies to manual setup only. Docker users should edit the mounts
140140-in `docker-compose.yml` instead.)
141141-142142-Right now, the database and repositories of your knot lives in `/home/git`. You
143143-can move these paths if you'd like to store them in another folder. Be careful
144144-when adjusting these paths:
145145-146146-* Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent
147147-any possible side effects. Remember to restart it once you're done.
148148-* Make backups before moving in case something goes wrong.
149149-* Make sure the `git` user can read and write from the new paths.
150150-151151-#### database
152152-153153-As an example, let's say the current database is at `/home/git/knotserver.db`,
154154-and we want to move it to `/home/git/database/knotserver.db`.
155155-156156-Copy the current database to the new location. Make sure to copy the `.db-shm`
157157-and `.db-wal` files if they exist.
158158-159159-```
160160-mkdir /home/git/database
161161-cp /home/git/knotserver.db* /home/git/database
162162-```
163163-164164-In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to
165165-the new file path (_not_ the directory):
166166-167167-```
168168-KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db
169169-```
170170-171171-#### repositories
172172-173173-As an example, let's say the repositories are currently in `/home/git`, and we
174174-want to move them into `/home/git/repositories`.
175175-176176-Create the new folder, then move the existing repositories (if there are any):
177177-178178-```
179179-mkdir /home/git/repositories
180180-# move all DIDs into the new folder; these will vary for you!
181181-mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories
182182-```
183183-184184-In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH`
185185-to the new directory:
186186-187187-```
188188-KNOT_REPO_SCAN_PATH=/home/git/repositories
189189-```
190190-191191-Similarly, update your `sshd` `AuthorizedKeysCommand` to use the updated
192192-repository path:
193193-194194-```
195195-sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
196196-Match User git
197197- AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories
198198- AuthorizedKeysCommandUser nobody
199199-EOF
200200-```
201201-202202-Make sure to restart your SSH server!
203203-204204-#### MOTD (message of the day)
205205-206206-To configure the MOTD used ("Welcome to this knot!" by default), edit the
207207-`/home/git/motd` file:
208208-209209-```
210210-printf "Hi from this knot!\n" > /home/git/motd
211211-```
212212-213213-Note that you should add a newline at the end if setting a non-empty message
214214-since the knot won't do this for you.
-59
docs/migrations.md
···11-# Migrations
22-33-This document is laid out in reverse-chronological order.
44-Newer migration guides are listed first, and older guides
55-are further down the page.
66-77-## Upgrading from v1.8.x
88-99-After v1.8.2, the HTTP API for knot and spindles have been
1010-deprecated and replaced with XRPC. Repositories on outdated
1111-knots will not be viewable from the appview. Upgrading is
1212-straightforward however.
1313-1414-For knots:
1515-1616-- Upgrade to latest tag (v1.9.0 or above)
1717-- Head to the [knot dashboard](https://tangled.org/settings/knots) and
1818- hit the "retry" button to verify your knot
1919-2020-For spindles:
2121-2222-- Upgrade to latest tag (v1.9.0 or above)
2323-- Head to the [spindle
2424- dashboard](https://tangled.org/settings/spindles) and hit the
2525- "retry" button to verify your spindle
2626-2727-## Upgrading from v1.7.x
2828-2929-After v1.7.0, knot secrets have been deprecated. You no
3030-longer need a secret from the appview to run a knot. All
3131-authorized commands to knots are managed via [Inter-Service
3232-Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
3333-Knots will be read-only until upgraded.
3434-3535-Upgrading is quite easy, in essence:
3636-3737-- `KNOT_SERVER_SECRET` is no more, you can remove this
3838- environment variable entirely
3939-- `KNOT_SERVER_OWNER` is now required on boot, set this to
4040- your DID. You can find your DID in the
4141- [settings](https://tangled.org/settings) page.
4242-- Restart your knot once you have replaced the environment
4343- variable
4444-- Head to the [knot dashboard](https://tangled.org/settings/knots) and
4545- hit the "retry" button to verify your knot. This simply
4646- writes a `sh.tangled.knot` record to your PDS.
4747-4848-If you use the nix module, simply bump the flake to the
4949-latest revision, and change your config block like so:
5050-5151-```diff
5252- services.tangled.knot = {
5353- enable = true;
5454- server = {
5555-- secretFile = /path/to/secret;
5656-+ owner = "did:plc:foo";
5757- };
5858- };
5959-```
-25
docs/spindle/architecture.md
···11-# spindle architecture
22-33-Spindle is a small CI runner service. Here's a high level overview of how it operates:
44-55-* listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
66-[`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream.
77-* when a new repo record comes through (typically when you add a spindle to a
88-repo from the settings), spindle then resolves the underlying knot and
99-subscribes to repo events (see:
1010-[`sh.tangled.pipeline`](/lexicons/pipeline.json)).
1111-* the spindle engine then handles execution of the pipeline, with results and
1212-logs beamed on the spindle event stream over wss
1313-1414-### the engine
1515-1616-At present, the only supported backend is Docker (and Podman, if Docker
1717-compatibility is enabled, so that `/run/docker.sock` is created). Spindle
1818-executes each step in the pipeline in a fresh container, with state persisted
1919-across steps within the `/tangled/workspace` directory.
2020-2121-The base image for the container is constructed on the fly using
2222-[Nixery](https://nixery.dev), which is handy for caching layers for frequently
2323-used packages.
2424-2525-The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
-52
docs/spindle/hosting.md
···11-# spindle self-hosting guide
22-33-## prerequisites
44-55-* Go
66-* Docker (the only supported backend currently)
77-88-## configuration
99-1010-Spindle is configured using environment variables. The following environment variables are available:
1111-1212-* `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`).
1313-* `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`).
1414-* `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required).
1515-* `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`).
1616-* `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`).
1717-* `SPINDLE_SERVER_OWNER`: The DID of the owner (required).
1818-* `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`).
1919-* `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`).
2020-* `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`).
2121-2222-## running spindle
2323-2424-1. **Set the environment variables.** For example:
2525-2626- ```shell
2727- export SPINDLE_SERVER_HOSTNAME="your-hostname"
2828- export SPINDLE_SERVER_OWNER="your-did"
2929- ```
3030-3131-2. **Build the Spindle binary.**
3232-3333- ```shell
3434- cd core
3535- go mod download
3636- go build -o cmd/spindle/spindle cmd/spindle/main.go
3737- ```
3838-3939-3. **Create the log directory.**
4040-4141- ```shell
4242- sudo mkdir -p /var/log/spindle
4343- sudo chown $USER:$USER -R /var/log/spindle
4444- ```
4545-4646-4. **Run the Spindle binary.**
4747-4848- ```shell
4949- ./cmd/spindle/spindle
5050- ```
5151-5252-Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
-285
docs/spindle/openbao.md
···11-# spindle secrets with openbao
22-33-This document covers setting up Spindle to use OpenBao for secrets
44-management via OpenBao Proxy instead of the default SQLite backend.
55-66-## overview
77-88-Spindle now uses OpenBao Proxy for secrets management. The proxy handles
99-authentication automatically using AppRole credentials, while Spindle
1010-connects to the local proxy instead of directly to the OpenBao server.
1111-1212-This approach provides better security, automatic token renewal, and
1313-simplified application code.
1414-1515-## installation
1616-1717-Install OpenBao from nixpkgs:
1818-1919-```bash
2020-nix shell nixpkgs#openbao # for a local server
2121-```
2222-2323-## setup
2424-2525-The setup process can is documented for both local development and production.
2626-2727-### local development
2828-2929-Start OpenBao in dev mode:
3030-3131-```bash
3232-bao server -dev -dev-root-token-id="root" -dev-listen-address=127.0.0.1:8201
3333-```
3434-3535-This starts OpenBao on `http://localhost:8201` with a root token.
3636-3737-Set up environment for bao CLI:
3838-3939-```bash
4040-export BAO_ADDR=http://localhost:8200
4141-export BAO_TOKEN=root
4242-```
4343-4444-### production
4545-4646-You would typically use a systemd service with a configuration file. Refer to
4747-[@tangled.org/infra](https://tangled.org/@tangled.org/infra) for how this can be
4848-achieved using Nix.
4949-5050-Then, initialize the bao server:
5151-```bash
5252-bao operator init -key-shares=1 -key-threshold=1
5353-```
5454-5555-This will print out an unseal key and a root key. Save them somewhere (like a password manager). Then unseal the vault to begin setting it up:
5656-```bash
5757-bao operator unseal <unseal_key>
5858-```
5959-6060-All steps below remain the same across both dev and production setups.
6161-6262-### configure openbao server
6363-6464-Create the spindle KV mount:
6565-6666-```bash
6767-bao secrets enable -path=spindle -version=2 kv
6868-```
6969-7070-Set up AppRole authentication and policy:
7171-7272-Create a policy file `spindle-policy.hcl`:
7373-7474-```hcl
7575-# Full access to spindle KV v2 data
7676-path "spindle/data/*" {
7777- capabilities = ["create", "read", "update", "delete"]
7878-}
7979-8080-# Access to metadata for listing and management
8181-path "spindle/metadata/*" {
8282- capabilities = ["list", "read", "delete", "update"]
8383-}
8484-8585-# Allow listing at root level
8686-path "spindle/" {
8787- capabilities = ["list"]
8888-}
8989-9090-# Required for connection testing and health checks
9191-path "auth/token/lookup-self" {
9292- capabilities = ["read"]
9393-}
9494-```
9595-9696-Apply the policy and create an AppRole:
9797-9898-```bash
9999-bao policy write spindle-policy spindle-policy.hcl
100100-bao auth enable approle
101101-bao write auth/approle/role/spindle \
102102- token_policies="spindle-policy" \
103103- token_ttl=1h \
104104- token_max_ttl=4h \
105105- bind_secret_id=true \
106106- secret_id_ttl=0 \
107107- secret_id_num_uses=0
108108-```
109109-110110-Get the credentials:
111111-112112-```bash
113113-# Get role ID (static)
114114-ROLE_ID=$(bao read -field=role_id auth/approle/role/spindle/role-id)
115115-116116-# Generate secret ID
117117-SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/spindle/secret-id)
118118-119119-echo "Role ID: $ROLE_ID"
120120-echo "Secret ID: $SECRET_ID"
121121-```
122122-123123-### create proxy configuration
124124-125125-Create the credential files:
126126-127127-```bash
128128-# Create directory for OpenBao files
129129-mkdir -p /tmp/openbao
130130-131131-# Save credentials
132132-echo "$ROLE_ID" > /tmp/openbao/role-id
133133-echo "$SECRET_ID" > /tmp/openbao/secret-id
134134-chmod 600 /tmp/openbao/role-id /tmp/openbao/secret-id
135135-```
136136-137137-Create a proxy configuration file `/tmp/openbao/proxy.hcl`:
138138-139139-```hcl
140140-# OpenBao server connection
141141-vault {
142142- address = "http://localhost:8200"
143143-}
144144-145145-# Auto-Auth using AppRole
146146-auto_auth {
147147- method "approle" {
148148- mount_path = "auth/approle"
149149- config = {
150150- role_id_file_path = "/tmp/openbao/role-id"
151151- secret_id_file_path = "/tmp/openbao/secret-id"
152152- }
153153- }
154154-155155- # Optional: write token to file for debugging
156156- sink "file" {
157157- config = {
158158- path = "/tmp/openbao/token"
159159- mode = 0640
160160- }
161161- }
162162-}
163163-164164-# Proxy listener for Spindle
165165-listener "tcp" {
166166- address = "127.0.0.1:8201"
167167- tls_disable = true
168168-}
169169-170170-# Enable API proxy with auto-auth token
171171-api_proxy {
172172- use_auto_auth_token = true
173173-}
174174-175175-# Enable response caching
176176-cache {
177177- use_auto_auth_token = true
178178-}
179179-180180-# Logging
181181-log_level = "info"
182182-```
183183-184184-### start the proxy
185185-186186-Start OpenBao Proxy:
187187-188188-```bash
189189-bao proxy -config=/tmp/openbao/proxy.hcl
190190-```
191191-192192-The proxy will authenticate with OpenBao and start listening on
193193-`127.0.0.1:8201`.
194194-195195-### configure spindle
196196-197197-Set these environment variables for Spindle:
198198-199199-```bash
200200-export SPINDLE_SERVER_SECRETS_PROVIDER=openbao
201201-export SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=http://127.0.0.1:8201
202202-export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle
203203-```
204204-205205-Start Spindle:
206206-207207-Spindle will now connect to the local proxy, which handles all
208208-authentication automatically.
209209-210210-## production setup for proxy
211211-212212-For production, you'll want to run the proxy as a service:
213213-214214-Place your production configuration in `/etc/openbao/proxy.hcl` with
215215-proper TLS settings for the vault connection.
216216-217217-## verifying setup
218218-219219-Test the proxy directly:
220220-221221-```bash
222222-# Check proxy health
223223-curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/sys/health
224224-225225-# Test token lookup through proxy
226226-curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/auth/token/lookup-self
227227-```
228228-229229-Test OpenBao operations through the server:
230230-231231-```bash
232232-# List all secrets
233233-bao kv list spindle/
234234-235235-# Add a test secret via Spindle API, then check it exists
236236-bao kv list spindle/repos/
237237-238238-# Get a specific secret
239239-bao kv get spindle/repos/your_repo_path/SECRET_NAME
240240-```
241241-242242-## how it works
243243-244244-- Spindle connects to OpenBao Proxy on localhost (typically port 8200 or 8201)
245245-- The proxy authenticates with OpenBao using AppRole credentials
246246-- All Spindle requests go through the proxy, which injects authentication tokens
247247-- Secrets are stored at `spindle/repos/{sanitized_repo_path}/{secret_key}`
248248-- Repository paths like `did:plc:alice/myrepo` become `did_plc_alice_myrepo`
249249-- The proxy handles all token renewal automatically
250250-- Spindle no longer manages tokens or authentication directly
251251-252252-## troubleshooting
253253-254254-**Connection refused**: Check that the OpenBao Proxy is running and
255255-listening on the configured address.
256256-257257-**403 errors**: Verify the AppRole credentials are correct and the policy
258258-has the necessary permissions.
259259-260260-**404 route errors**: The spindle KV mount probably doesn't exist - run
261261-the mount creation step again.
262262-263263-**Proxy authentication failures**: Check the proxy logs and verify the
264264-role-id and secret-id files are readable and contain valid credentials.
265265-266266-**Secret not found after writing**: This can indicate policy permission
267267-issues. Verify the policy includes both `spindle/data/*` and
268268-`spindle/metadata/*` paths with appropriate capabilities.
269269-270270-Check proxy logs:
271271-272272-```bash
273273-# If running as systemd service
274274-journalctl -u openbao-proxy -f
275275-276276-# If running directly, check the console output
277277-```
278278-279279-Test AppRole authentication manually:
280280-281281-```bash
282282-bao write auth/approle/login \
283283- role_id="$(cat /tmp/openbao/role-id)" \
284284- secret_id="$(cat /tmp/openbao/secret-id)"
285285-```
-183
docs/spindle/pipeline.md
···11-# spindle pipelines
22-33-Spindle workflows allow you to write CI/CD pipelines in a simple format. They're located in the `.tangled/workflows` directory at the root of your repository, and are defined using YAML.
44-55-The fields are:
66-77-- [Trigger](#trigger): A **required** field that defines when a workflow should be triggered.
88-- [Engine](#engine): A **required** field that defines which engine a workflow should run on.
99-- [Clone options](#clone-options): An **optional** field that defines how the repository should be cloned.
1010-- [Dependencies](#dependencies): An **optional** field that allows you to list dependencies you may need.
1111-- [Environment](#environment): An **optional** field that allows you to define environment variables.
1212-- [Steps](#steps): An **optional** field that allows you to define what steps should run in the workflow.
1313-1414-## Trigger
1515-1616-The first thing to add to a workflow is the trigger, which defines when a workflow runs. This is defined using a `when` field, which takes in a list of conditions. Each condition has the following fields:
1717-1818-- `event`: This is a **required** field that defines when your workflow should run. It's a list that can take one or more of the following values:
1919- - `push`: The workflow should run every time a commit is pushed to the repository.
2020- - `pull_request`: The workflow should run every time a pull request is made or updated.
2121- - `manual`: The workflow can be triggered manually.
2222-- `branch`: Defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event. Supports glob patterns using `*` and `**` (e.g., `main`, `develop`, `release-*`). Either `branch` or `tag` (or both) must be specified for `push` events.
2323-- `tag`: Defines which tags the workflow should run for. Only used with the `push` event - when tags matching the pattern(s) listed here are pushed, the workflow will trigger. This field has no effect with `pull_request` or `manual` events. Supports glob patterns using `*` and `**` (e.g., `v*`, `v1.*`, `release-**`). Either `branch` or `tag` (or both) must be specified for `push` events.
2424-2525-For example, if you'd like to define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
2626-2727-```yaml
2828-when:
2929- - event: ["push", "manual"]
3030- branch: ["main", "develop"]
3131- - event: ["pull_request"]
3232- branch: ["main"]
3333-```
3434-3535-You can also trigger workflows on tag pushes. For instance, to run a deployment workflow when tags matching `v*` are pushed:
3636-3737-```yaml
3838-when:
3939- - event: ["push"]
4040- tag: ["v*"]
4141-```
4242-4343-You can even combine branch and tag patterns in a single constraint (the workflow triggers if either matches):
4444-4545-```yaml
4646-when:
4747- - event: ["push"]
4848- branch: ["main", "release-*"]
4949- tag: ["v*", "stable"]
5050-```
5151-5252-## Engine
5353-5454-Next is the engine on which the workflow should run, defined using the **required** `engine` field. The currently supported engines are:
5555-5656-- `nixery`: This uses an instance of [Nixery](https://nixery.dev) to run steps, which allows you to add [dependencies](#dependencies) from [Nixpkgs](https://github.com/NixOS/nixpkgs). You can search for packages on https://search.nixos.org, and there's a pretty good chance the package(s) you're looking for will be there.
5757-5858-Example:
5959-6060-```yaml
6161-engine: "nixery"
6262-```
6363-6464-## Clone options
6565-6666-When a workflow starts, the first step is to clone the repository. You can customize this behavior using the **optional** `clone` field. It has the following fields:
6767-6868-- `skip`: Setting this to `true` will skip cloning the repository. This can be useful if your workflow is doing something that doesn't require anything from the repository itself. This is `false` by default.
6969-- `depth`: This sets the number of commits, or the "clone depth", to fetch from the repository. For example, if you set this to 2, the last 2 commits will be fetched. By default, the depth is set to 1, meaning only the most recent commit will be fetched, which is the commit that triggered the workflow.
7070-- `submodules`: If you use [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your repository, setting this field to `true` will recursively fetch all submodules. This is `false` by default.
7171-7272-The default settings are:
7373-7474-```yaml
7575-clone:
7676- skip: false
7777- depth: 1
7878- submodules: false
7979-```
8080-8181-## Dependencies
8282-8383-Usually when you're running a workflow, you'll need additional dependencies. The `dependencies` field lets you define which dependencies to get, and from where. It's a key-value map, with the key being the registry to fetch dependencies from, and the value being the list of dependencies to fetch.
8484-8585-Say you want to fetch Node.js and Go from `nixpkgs`, and a package called `my_pkg` you've made from your own registry at your repository at `https://tangled.sh/@example.com/my_pkg`. You can define those dependencies like so:
8686-8787-```yaml
8888-dependencies:
8989- # nixpkgs
9090- nixpkgs:
9191- - nodejs
9292- - go
9393- # custom registry
9494- git+https://tangled.org/@example.com/my_pkg:
9595- - my_pkg
9696-```
9797-9898-Now these dependencies are available to use in your workflow!
9999-100100-## Environment
101101-102102-The `environment` field allows you define environment variables that will be available throughout the entire workflow. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.**
103103-104104-Example:
105105-106106-```yaml
107107-environment:
108108- GOOS: "linux"
109109- GOARCH: "arm64"
110110- NODE_ENV: "production"
111111- MY_ENV_VAR: "MY_ENV_VALUE"
112112-```
113113-114114-## Steps
115115-116116-The `steps` field allows you to define what steps should run in the workflow. It's a list of step objects, each with the following fields:
117117-118118-- `name`: This field allows you to give your step a name. This name is visible in your workflow runs, and is used to describe what the step is doing.
119119-- `command`: This field allows you to define a command to run in that step. The step is run in a Bash shell, and the logs from the command will be visible in the pipelines page on the Tangled website. The [dependencies](#dependencies) you added will be available to use here.
120120-- `environment`: Similar to the global [environment](#environment) config, this **optional** field is a key-value map that allows you to set environment variables for the step. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.**
121121-122122-Example:
123123-124124-```yaml
125125-steps:
126126- - name: "Build backend"
127127- command: "go build"
128128- environment:
129129- GOOS: "darwin"
130130- GOARCH: "arm64"
131131- - name: "Build frontend"
132132- command: "npm run build"
133133- environment:
134134- NODE_ENV: "production"
135135-```
136136-137137-## Complete workflow
138138-139139-```yaml
140140-# .tangled/workflows/build.yml
141141-142142-when:
143143- - event: ["push", "manual"]
144144- branch: ["main", "develop"]
145145- - event: ["pull_request"]
146146- branch: ["main"]
147147-148148-engine: "nixery"
149149-150150-# using the default values
151151-clone:
152152- skip: false
153153- depth: 1
154154- submodules: false
155155-156156-dependencies:
157157- # nixpkgs
158158- nixpkgs:
159159- - nodejs
160160- - go
161161- # custom registry
162162- git+https://tangled.org/@example.com/my_pkg:
163163- - my_pkg
164164-165165-environment:
166166- GOOS: "linux"
167167- GOARCH: "arm64"
168168- NODE_ENV: "production"
169169- MY_ENV_VAR: "MY_ENV_VALUE"
170170-171171-steps:
172172- - name: "Build backend"
173173- command: "go build"
174174- environment:
175175- GOOS: "darwin"
176176- GOARCH: "arm64"
177177- - name: "Build frontend"
178178- command: "npm run build"
179179- environment:
180180- NODE_ENV: "production"
181181-```
182182-183183-If you want another example of a workflow, you can look at the one [Tangled uses to build the project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
···11+package db
22+33+import (
44+ "context"
55+ "database/sql"
66+ "log/slog"
77+ "strings"
88+99+ _ "github.com/mattn/go-sqlite3"
1010+ "tangled.org/core/log"
1111+)
1212+1313+type DB struct {
1414+ db *sql.DB
1515+ logger *slog.Logger
1616+}
1717+1818+func Setup(ctx context.Context, dbPath string) (*DB, error) {
1919+ // https://github.com/mattn/go-sqlite3#connection-string
2020+ opts := []string{
2121+ "_foreign_keys=1",
2222+ "_journal_mode=WAL",
2323+ "_synchronous=NORMAL",
2424+ "_auto_vacuum=incremental",
2525+ }
2626+2727+ logger := log.FromContext(ctx)
2828+ logger = log.SubLogger(logger, "db")
2929+3030+ db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
3131+ if err != nil {
3232+ return nil, err
3333+ }
3434+3535+ conn, err := db.Conn(ctx)
3636+ if err != nil {
3737+ return nil, err
3838+ }
3939+ defer conn.Close()
4040+4141+ _, err = conn.ExecContext(ctx, `
4242+ create table if not exists known_dids (
4343+ did text primary key
4444+ );
4545+4646+ create table if not exists public_keys (
4747+ id integer primary key autoincrement,
4848+ did text not null,
4949+ key text not null,
5050+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
5151+ unique(did, key),
5252+ foreign key (did) references known_dids(did) on delete cascade
5353+ );
5454+5555+ create table if not exists _jetstream (
5656+ id integer primary key autoincrement,
5757+ last_time_us integer not null
5858+ );
5959+6060+ create table if not exists events (
6161+ rkey text not null,
6262+ nsid text not null,
6363+ event text not null, -- json
6464+ created integer not null default (strftime('%s', 'now')),
6565+ primary key (rkey, nsid)
6666+ );
6767+6868+ create table if not exists migrations (
6969+ id integer primary key autoincrement,
7070+ name text unique
7171+ );
7272+ `)
7373+ if err != nil {
7474+ return nil, err
7575+ }
7676+7777+ return &DB{
7878+ db: db,
7979+ logger: logger,
8080+ }, nil
8181+}
-64
knotserver/db/init.go
···11-package db
22-33-import (
44- "database/sql"
55- "strings"
66-77- _ "github.com/mattn/go-sqlite3"
88-)
99-1010-type DB struct {
1111- db *sql.DB
1212-}
1313-1414-func Setup(dbPath string) (*DB, error) {
1515- // https://github.com/mattn/go-sqlite3#connection-string
1616- opts := []string{
1717- "_foreign_keys=1",
1818- "_journal_mode=WAL",
1919- "_synchronous=NORMAL",
2020- "_auto_vacuum=incremental",
2121- }
2222-2323- db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
2424- if err != nil {
2525- return nil, err
2626- }
2727-2828- // NOTE: If any other migration is added here, you MUST
2929- // copy the pattern in appview: use a single sql.Conn
3030- // for every migration.
3131-3232- _, err = db.Exec(`
3333- create table if not exists known_dids (
3434- did text primary key
3535- );
3636-3737- create table if not exists public_keys (
3838- id integer primary key autoincrement,
3939- did text not null,
4040- key text not null,
4141- created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
4242- unique(did, key),
4343- foreign key (did) references known_dids(did) on delete cascade
4444- );
4545-4646- create table if not exists _jetstream (
4747- id integer primary key autoincrement,
4848- last_time_us integer not null
4949- );
5050-5151- create table if not exists events (
5252- rkey text not null,
5353- nsid text not null,
5454- event text not null, -- json
5555- created integer not null default (strftime('%s', 'now')),
5656- primary key (rkey, nsid)
5757- );
5858- `)
5959- if err != nil {
6060- return nil, err
6161- }
6262-6363- return &DB{db: db}, nil
6464-}