{
"path": "/3m23cshg4e225",
"site": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/site.standard.publication/3lwagtcm36s2d",
"$type": "site.standard.document",
"title": "Deploying Statusphere to Railway from Tangled",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Saw some discussion today about how to deploy Atproto apps for the less backend/DevOps-inclined. Railway seemed pretty popular, and I've been meaning to try it myself, so let's see what damage we can do."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 20,
"byteStart": 13
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "The absolute easiest way to deploy your first Atproto app is by using the Railway template @samuel.bsky.team created -- it only requires a couple of clicks!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"src": "https://railway.com/deploy/atproto-statusphere-app?referralCode=e99Eop",
"$type": "pub.leaflet.blocks.website",
"title": "Deploy atproto statusphere app",
"description": "Deploy atproto statusphere app on Railway with one click, start for free. A minimal demo of an end-to-end atproto application",
"previewImage": {
"$type": "blob",
"ref": {
"$link": "bafkreibr4crcylrr5efr7l7tdaq6tz6aonooqh3jddxxf5tojg6zlggi3m"
},
"mimeType": "image/png",
"size": 26907
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "This is the first in a multi-part series:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "1. Deploying statusphere-react to Railway via CLI (you're here)"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 53,
"byteStart": 3
},
"features": [
{
"uri": "https://graham.leaflet.pub/3m2uniaiph22c",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "2. Automating Railway deployments via Tangled/Spindle"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "1. Clone statusphere-react"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "You can either clone the repo as-is for this tutorial, or you can fork it and clone your fork."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"src": "https://tangled.org/@samuel.bsky.team/statusphere-react",
"$type": "pub.leaflet.blocks.website",
"title": "@samuel.bsky.team/statusphere-react",
"description": "the statusphere demo reworked into a vite/react app in a monorepo",
"previewImage": {
"$type": "blob",
"ref": {
"$link": "bafkreieuqkru3fza7aow2j5wksutgbsyjhlqaexi5zpgvtismhpuc6afv4"
},
"mimeType": "image/png",
"size": 13900
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "2. Install Railway CLI"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 67,
"byteStart": 10
},
"features": [
{
"uri": "https://docs.railway.com/guides/cli",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 104,
"byteStart": 88
},
"features": [
{
"uri": "https://search.nixos.org/packages?channel=25.05&show=railway&query=railway",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Check the docs for how to install the Railway CLI for your platform. For the Nix folks, there's a nixpkg."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "3. Log in to Railway"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 55,
"byteStart": 36
},
"features": [
{
"uri": "https://railway.app",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "I'm assuming you've already gone to https://railway.app and made an account."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Navigate to your statusphere-react directory, and then run the following:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway login",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 14
},
"features": [
{
"uri": "https://docs.railway.com/guides/cli#authenticating-with-the-cli",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Check out the CLI login docs if something goes wrong here."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "4. Create a new project"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 9,
"byteStart": 2
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "A Project is the top-level resource for organizing your app. Run the following, and give your project a name if you like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway init",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "5. Create a new service"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 8
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Next, a Service is an instance of your app. We're going to first create an empty service, so we can configure our environment variables later."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Run the following, and select \"Empty Service\" when prompted. Don't worry about setting variables, or setting a name if you don't want to:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway add",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "6. Create a domain"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "To properly configure our OAuth client, we'll need a domain."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Railway makes getting a service-specific domain easy; save this value and hold on to it for later:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway domain",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "7. Set up a volume"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "A volume ensures that our database persists between deployments and restarts."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 66,
"byteStart": 55
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Let's make one with the following command, and specify /persistent as the mount path when prompted:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway volume add",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "8. Set our environment variables"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Okay, we should have everything we need now to deploy very own statusphere."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "We can set all of our basics at once:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway variables \\\n --set NODE_ENV=\"production\" \\\n --set PORT=\"8080\" \\\n --set DB_PATH=\"../../../persistent/data.sqlite\"",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 27,
"byteStart": 14
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "We'll set our COOKIE_SECRET to a randomly-generated 32-character value:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway variables --set COOKIE_SECRET=$(openssl rand -base64 32)",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 33,
"byteStart": 23
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 42,
"byteStart": 38
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 88,
"byteStart": 84
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 135,
"byteStart": 127
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 192,
"byteStart": 138
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 195,
"byteStart": 194
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Finally, we'll set our PUBLIC_URL and HOST to that domain we generated earlier. For HOST, remove the protocol from the domain (https://). Make sure that neither value contains trailing slashes (/):"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway variables \\\n --set PUBLIC_URL=\"<your url>\" \\\n --set HOST=\"<your url minus protocol>\"",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "9. Deploy!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Okay, we should be all good to go. Running the following command will deploy our app, and follow along with the logs:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "railway up",
"syntaxHighlightingTheme": "rose-pine"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "That wraps it all up! You should be able to navigate to your domain, sign in to your account, and watch the statuses roll in."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Now that we've done all the setup and heavy lifting, CI/CD on Tangled should be a breeze. I'll update this Leaflet with a link once I'm done."
}
}
]
}
]
},
"bskyPostRef": {
"cid": "bafyreibbh5ugpjvyvz7mpzjkgrldqopmxw2sapomwwvjravgo2uttwcuky",
"uri": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/app.bsky.feed.post/3m23csnd2as25",
"commit": {
"cid": "bafyreib4fpdu3swnaoto7dsijprxfksaclllv5a4xsabtc7dja3ey4n5fa",
"rev": "3m23csnfhrc2l"
},
"validationStatus": "valid"
},
"description": "Part 1: Local machine to Railway",
"publishedAt": "2025-09-30T19:59:33.448Z"
}