[{"data":1,"prerenderedAt":711},["ShallowReactive",2],{"navigation":3,"\u002Fself-hosting\u002Frelay":87,"\u002Fself-hosting\u002Frelay-surround":706},[4,32,51,69],{"title":5,"path":6,"stem":7,"children":8,"icon":31},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,11,15,19,23,27],{"title":10,"path":6,"stem":7},"What is this?",{"title":12,"path":13,"stem":14},"Before You Begin","\u002Fgetting-started\u002Fbefore-you-begin","1.getting-started\u002F2.before-you-begin",{"title":16,"path":17,"stem":18},"Install the Apps","\u002Fgetting-started\u002Finstall","1.getting-started\u002F3.install",{"title":20,"path":21,"stem":22},"Your First Family Tree","\u002Fgetting-started\u002Fyour-first-tree","1.getting-started\u002F4.your-first-tree",{"title":24,"path":25,"stem":26},"Invite Family Members","\u002Fgetting-started\u002Finvite-family","1.getting-started\u002F5.invite-family",{"title":28,"path":29,"stem":30},"Connect Family Branches","\u002Fgetting-started\u002Fconnect-branches","1.getting-started\u002F6.connect-branches","i-lucide-footprints",{"title":33,"icon":34,"path":35,"stem":36,"children":37,"page":50},"Apps","i-lucide-smartphone","\u002Fapps","2.apps",[38,42,46],{"title":39,"path":40,"stem":41},"Iwacu — Family Tree","\u002Fapps\u002Fiwacu","2.apps\u002F1.iwacu",{"title":43,"path":44,"stem":45},"Isoko — Local Marketplace","\u002Fapps\u002Fisoko","2.apps\u002F2.isoko",{"title":47,"path":48,"stem":49},"Abantu — Community Governance","\u002Fapps\u002Fabantu","2.apps\u002F3.abantu",false,{"title":52,"icon":53,"path":54,"stem":55,"children":56,"page":50},"Self-Hosting","i-lucide-server","\u002Fself-hosting","3.self-hosting",[57,61,65],{"title":58,"path":59,"stem":60},"Self-Hosting Overview","\u002Fself-hosting\u002Foverview","3.self-hosting\u002F1.overview",{"title":62,"path":63,"stem":64},"Deploy the Relay Server","\u002Fself-hosting\u002Frelay","3.self-hosting\u002F2.relay",{"title":66,"path":67,"stem":68},"Configure Tor Hidden Service","\u002Fself-hosting\u002Ftor","3.self-hosting\u002F3.tor",{"title":70,"icon":71,"path":72,"stem":73,"children":74,"page":50},"Privacy & Security","i-lucide-shield","\u002Fprivacy","4.privacy",[75,79,83],{"title":76,"path":77,"stem":78},"How Privacy Works Here","\u002Fprivacy\u002Fhow-it-works","4.privacy\u002F1.how-it-works",{"title":80,"path":81,"stem":82},"Device Security","\u002Fprivacy\u002Fdevice","4.privacy\u002F2.device",{"title":84,"path":85,"stem":86},"Under Pressure","\u002Fprivacy\u002Fcoercion","4.privacy\u002F3.coercion",{"id":88,"title":62,"body":89,"description":700,"extension":701,"links":702,"meta":703,"navigation":217,"path":63,"seo":704,"stem":64,"__hash__":705},"docs\u002F3.self-hosting\u002F2.relay.md",{"type":90,"value":91,"toc":690},"minimark",[92,96,108,113,116,152,156,159,175,182,292,296,315,318,331,334,368,372,375,433,436,439,469,472,476,484,487,570,578,583,587,590,593,613,617,648,651,655,686],[93,94,62],"h1",{"id":95},"deploy-the-relay-server",[97,98,99,100,107],"p",{},"This guide assumes you have a Linux server with Docker and Docker Compose installed. If not, install them first: ",[101,102,106],"a",{"href":103,"rel":104},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Finstall\u002F",[105],"nofollow","docs.docker.com\u002Fengine\u002Finstall",".",[109,110,112],"h2",{"id":111},"get-the-code","Get the code",[97,114,115],{},"Clone the repository to your server:",[117,118,123],"pre",{"className":119,"code":120,"language":121,"meta":122,"style":122},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https:\u002F\u002Fgithub.com\u002Fyour-org\u002Fproto.git\ncd proto\u002Finfra\u002Frelay\n","bash","",[124,125,126,142],"code",{"__ignoreMap":122},[127,128,131,135,139],"span",{"class":129,"line":130},"line",1,[127,132,134],{"class":133},"sBMFI","git",[127,136,138],{"class":137},"sfazB"," clone",[127,140,141],{"class":137}," https:\u002F\u002Fgithub.com\u002Fyour-org\u002Fproto.git\n",[127,143,145,149],{"class":129,"line":144},2,[127,146,148],{"class":147},"s2Zo4","cd",[127,150,151],{"class":137}," proto\u002Finfra\u002Frelay\n",[109,153,155],{"id":154},"configure","Configure",[97,157,158],{},"Copy the example environment file:",[117,160,162],{"className":119,"code":161,"language":121,"meta":122,"style":122},"cp .env.example .env\n",[124,163,164],{"__ignoreMap":122},[127,165,166,169,172],{"class":129,"line":130},[127,167,168],{"class":133},"cp",[127,170,171],{"class":137}," .env.example",[127,173,174],{"class":137}," .env\n",[97,176,177,178,181],{},"Edit ",[124,179,180],{},".env",":",[117,183,185],{"className":119,"code":184,"language":121,"meta":122,"style":122},"# The public URL your relay is reachable at.\n# If using Tor, this will be your .onion address (set after tor setup).\nRELAY_BASE_URL=https:\u002F\u002Fyour-server.example.com\n\n# The name shown on the invitation landing page.\nAPP_NAME=My Family Tree\n\n# Token expiry in hours. 72 hours is the default.\nTOKEN_TTL_HOURS=72\n\n# Port the relay listens on inside the container.\n# You do not need to change this unless you have a port conflict.\nRELAY_PORT=3000\n",[124,186,187,193,198,212,219,225,242,247,253,264,269,275,281],{"__ignoreMap":122},[127,188,189],{"class":129,"line":130},[127,190,192],{"class":191},"sHwdD","# The public URL your relay is reachable at.\n",[127,194,195],{"class":129,"line":144},[127,196,197],{"class":191},"# If using Tor, this will be your .onion address (set after tor setup).\n",[127,199,201,205,209],{"class":129,"line":200},3,[127,202,204],{"class":203},"sTEyZ","RELAY_BASE_URL",[127,206,208],{"class":207},"sMK4o","=",[127,210,211],{"class":137},"https:\u002F\u002Fyour-server.example.com\n",[127,213,215],{"class":129,"line":214},4,[127,216,218],{"emptyLinePlaceholder":217},true,"\n",[127,220,222],{"class":129,"line":221},5,[127,223,224],{"class":191},"# The name shown on the invitation landing page.\n",[127,226,228,231,233,236,239],{"class":129,"line":227},6,[127,229,230],{"class":203},"APP_NAME",[127,232,208],{"class":207},[127,234,235],{"class":137},"My",[127,237,238],{"class":133}," Family",[127,240,241],{"class":137}," Tree\n",[127,243,245],{"class":129,"line":244},7,[127,246,218],{"emptyLinePlaceholder":217},[127,248,250],{"class":129,"line":249},8,[127,251,252],{"class":191},"# Token expiry in hours. 72 hours is the default.\n",[127,254,256,259,261],{"class":129,"line":255},9,[127,257,258],{"class":203},"TOKEN_TTL_HOURS",[127,260,208],{"class":207},[127,262,263],{"class":137},"72\n",[127,265,267],{"class":129,"line":266},10,[127,268,218],{"emptyLinePlaceholder":217},[127,270,272],{"class":129,"line":271},11,[127,273,274],{"class":191},"# Port the relay listens on inside the container.\n",[127,276,278],{"class":129,"line":277},12,[127,279,280],{"class":191},"# You do not need to change this unless you have a port conflict.\n",[127,282,284,287,289],{"class":129,"line":283},13,[127,285,286],{"class":203},"RELAY_PORT",[127,288,208],{"class":207},[127,290,291],{"class":137},"3000\n",[109,293,295],{"id":294},"start-the-relay","Start the relay",[117,297,299],{"className":119,"code":298,"language":121,"meta":122,"style":122},"docker compose up -d\n",[124,300,301],{"__ignoreMap":122},[127,302,303,306,309,312],{"class":129,"line":130},[127,304,305],{"class":133},"docker",[127,307,308],{"class":137}," compose",[127,310,311],{"class":137}," up",[127,313,314],{"class":137}," -d\n",[97,316,317],{},"Check that it is running:",[117,319,321],{"className":119,"code":320,"language":121,"meta":122,"style":122},"curl http:\u002F\u002Flocalhost:3000\u002Fhealth\n",[124,322,323],{"__ignoreMap":122},[127,324,325,328],{"class":129,"line":130},[127,326,327],{"class":133},"curl",[127,329,330],{"class":137}," http:\u002F\u002Flocalhost:3000\u002Fhealth\n",[97,332,333],{},"Expected response:",[117,335,339],{"className":336,"code":337,"language":338,"meta":122,"style":122},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{ \"status\": \"ok\" }\n","json",[124,340,341],{"__ignoreMap":122},[127,342,343,346,349,353,356,358,360,363,365],{"class":129,"line":130},[127,344,345],{"class":207},"{",[127,347,348],{"class":207}," \"",[127,350,352],{"class":351},"spNyl","status",[127,354,355],{"class":207},"\"",[127,357,181],{"class":207},[127,359,348],{"class":207},[127,361,362],{"class":137},"ok",[127,364,355],{"class":207},[127,366,367],{"class":207}," }\n",[109,369,371],{"id":370},"test-a-token-flow","Test a token flow",[97,373,374],{},"Post a test token:",[117,376,378],{"className":119,"code":377,"language":121,"meta":122,"style":122},"curl -s -X POST http:\u002F\u002Flocalhost:3000\u002Fapi\u002Ftoken \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"ciphertext\":\"dGVzdA\",\"ttlHours\":1}' | jq\n",[124,379,380,399,413],{"__ignoreMap":122},[127,381,382,384,387,390,393,396],{"class":129,"line":130},[127,383,327],{"class":133},[127,385,386],{"class":137}," -s",[127,388,389],{"class":137}," -X",[127,391,392],{"class":137}," POST",[127,394,395],{"class":137}," http:\u002F\u002Flocalhost:3000\u002Fapi\u002Ftoken",[127,397,398],{"class":203}," \\\n",[127,400,401,404,406,409,411],{"class":129,"line":144},[127,402,403],{"class":137},"  -H",[127,405,348],{"class":207},[127,407,408],{"class":137},"Content-Type: application\u002Fjson",[127,410,355],{"class":207},[127,412,398],{"class":203},[127,414,415,418,421,424,427,430],{"class":129,"line":200},[127,416,417],{"class":137},"  -d",[127,419,420],{"class":207}," '",[127,422,423],{"class":137},"{\"ciphertext\":\"dGVzdA\",\"ttlHours\":1}",[127,425,426],{"class":207},"'",[127,428,429],{"class":207}," |",[127,431,432],{"class":133}," jq\n",[97,434,435],{},"You should receive a response with an invite URL.",[97,437,438],{},"Claim the token:",[117,440,442],{"className":119,"code":441,"language":121,"meta":122,"style":122},"curl -s http:\u002F\u002Flocalhost:3000\u002Fapi\u002Finvite\u002F\u003Ctoken-from-above> | jq\n",[124,443,444],{"__ignoreMap":122},[127,445,446,448,450,453,456,459,462,465,467],{"class":129,"line":130},[127,447,327],{"class":133},[127,449,386],{"class":137},[127,451,452],{"class":137}," http:\u002F\u002Flocalhost:3000\u002Fapi\u002Finvite\u002F",[127,454,455],{"class":207},"\u003C",[127,457,458],{"class":137},"token-from-abov",[127,460,461],{"class":203},"e",[127,463,464],{"class":207},">",[127,466,429],{"class":207},[127,468,432],{"class":133},[97,470,471],{},"The ciphertext should be returned and the token deleted. Claiming again should return a 404.",[109,473,475],{"id":474},"expose-the-relay","Expose the relay",[477,478,481],"callout",{"color":479,"icon":480},"amber","i-lucide-alert-triangle",[97,482,483],{},"Do not expose the relay directly with a public IP and no other protection. The relay has no authentication on who can post tokens. Rate limiting is built in, but you should put it behind a reverse proxy (nginx or Caddy) with TLS, or preferably behind Tor.",[97,485,486],{},"If you want HTTPS with a domain name, set up Caddy in front of the relay:",[117,488,492],{"className":489,"code":490,"language":491,"meta":122,"style":122},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Add to docker-compose.yml under services:\ncaddy:\n  image: caddy:alpine\n  ports:\n    - \"80:80\"\n    - \"443:443\"\n  volumes:\n    - .\u002FCaddyfile:\u002Fetc\u002Fcaddy\u002FCaddyfile\n    - caddy-data:\u002Fdata\n","yaml",[124,493,494,499,508,518,525,538,549,556,563],{"__ignoreMap":122},[127,495,496],{"class":129,"line":130},[127,497,498],{"class":191},"# Add to docker-compose.yml under services:\n",[127,500,501,505],{"class":129,"line":144},[127,502,504],{"class":503},"swJcz","caddy",[127,506,507],{"class":207},":\n",[127,509,510,513,515],{"class":129,"line":200},[127,511,512],{"class":503},"  image",[127,514,181],{"class":207},[127,516,517],{"class":137}," caddy:alpine\n",[127,519,520,523],{"class":129,"line":214},[127,521,522],{"class":503},"  ports",[127,524,507],{"class":207},[127,526,527,530,532,535],{"class":129,"line":221},[127,528,529],{"class":207},"    -",[127,531,348],{"class":207},[127,533,534],{"class":137},"80:80",[127,536,537],{"class":207},"\"\n",[127,539,540,542,544,547],{"class":129,"line":227},[127,541,529],{"class":207},[127,543,348],{"class":207},[127,545,546],{"class":137},"443:443",[127,548,537],{"class":207},[127,550,551,554],{"class":129,"line":244},[127,552,553],{"class":503},"  volumes",[127,555,507],{"class":207},[127,557,558,560],{"class":129,"line":249},[127,559,529],{"class":207},[127,561,562],{"class":137}," .\u002FCaddyfile:\u002Fetc\u002Fcaddy\u002FCaddyfile\n",[127,564,565,567],{"class":129,"line":255},[127,566,529],{"class":207},[127,568,569],{"class":137}," caddy-data:\u002Fdata\n",[117,571,576],{"className":572,"code":574,"language":575},[573],"language-text","# Caddyfile\nyour-server.example.com {\n    reverse_proxy relay:3000\n}\n","text",[124,577,574],{"__ignoreMap":122},[97,579,580,581,107],{},"For Tor configuration (recommended over public HTTPS), see ",[101,582,66],{"href":67},[109,584,586],{"id":585},"logs","Logs",[97,588,589],{},"The relay logs nothing about tokens or IP addresses by design. The only log output is startup confirmation and health check responses.",[97,591,592],{},"To view operational logs:",[117,594,596],{"className":119,"code":595,"language":121,"meta":122,"style":122},"docker compose logs -f relay\n",[124,597,598],{"__ignoreMap":122},[127,599,600,602,604,607,610],{"class":129,"line":130},[127,601,305],{"class":133},[127,603,308],{"class":137},[127,605,606],{"class":137}," logs",[127,608,609],{"class":137}," -f",[127,611,612],{"class":137}," relay\n",[109,614,616],{"id":615},"stop-and-restart","Stop and restart",[117,618,620],{"className":119,"code":619,"language":121,"meta":122,"style":122},"docker compose down    # stop\ndocker compose up -d   # start\n",[124,621,622,634],{"__ignoreMap":122},[127,623,624,626,628,631],{"class":129,"line":130},[127,625,305],{"class":133},[127,627,308],{"class":137},[127,629,630],{"class":137}," down",[127,632,633],{"class":191},"    # stop\n",[127,635,636,638,640,642,645],{"class":129,"line":144},[127,637,305],{"class":133},[127,639,308],{"class":137},[127,641,311],{"class":137},[127,643,644],{"class":137}," -d",[127,646,647],{"class":191},"   # start\n",[97,649,650],{},"Token data lives in memory. Pending tokens are lost on restart. This is intentional — restart clears all pending invitations. Tell your users to regenerate any pending links after a server restart.",[109,652,654],{"id":653},"update","Update",[117,656,658],{"className":119,"code":657,"language":121,"meta":122,"style":122},"git pull\ndocker compose build\ndocker compose up -d\n",[124,659,660,667,676],{"__ignoreMap":122},[127,661,662,664],{"class":129,"line":130},[127,663,134],{"class":133},[127,665,666],{"class":137}," pull\n",[127,668,669,671,673],{"class":129,"line":144},[127,670,305],{"class":133},[127,672,308],{"class":137},[127,674,675],{"class":137}," build\n",[127,677,678,680,682,684],{"class":129,"line":200},[127,679,305],{"class":133},[127,681,308],{"class":137},[127,683,311],{"class":137},[127,685,314],{"class":137},[687,688,689],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":122,"searchDepth":130,"depth":144,"links":691},[692,693,694,695,696,697,698,699],{"id":111,"depth":144,"text":112},{"id":154,"depth":144,"text":155},{"id":294,"depth":144,"text":295},{"id":370,"depth":144,"text":371},{"id":474,"depth":144,"text":475},{"id":585,"depth":144,"text":586},{"id":615,"depth":144,"text":616},{"id":653,"depth":144,"text":654},"How to run the invite relay with Docker Compose. Takes about 15 minutes on a fresh server.","md",null,{},{"title":62,"description":700},"ZjDa-NylOq0mmarszcqO8Fdb03QnJ9aiQAQ4o-nNEb8",[707,709],{"title":58,"path":59,"stem":60,"description":708,"children":-1},"What it means to run your own relay server, why you might want to, and what is required.",{"title":66,"path":67,"stem":68,"description":710,"children":-1},"How to make your relay reachable only via a .onion address. Nobody sees the server's IP — not the user, not their ISP, not the platform they use to share the link.",1783593685810]