[{"data":1,"prerenderedAt":906},["ShallowReactive",2],{"\u002Fdocs\u002Fdevelopment":3},{"id":4,"title":5,"body":6,"description":898,"extension":899,"meta":900,"navigation":901,"path":902,"seo":903,"stem":904,"__hash__":905},"docs\u002Fdocs\u002Fdevelopment.md","Development Setup",{"type":7,"value":8,"toc":883},"minimark",[9,13,28,33,58,62,137,140,205,209,233,240,263,266,298,316,320,323,362,369,373,376,397,402,405,440,444,447,472,486,490,518,541,556,580,584,641,645,765,769,784,814,867,871,879],[10,11,5],"h1",{"id":12},"development-setup",[14,15,16,17,21,22,27],"p",{},"This guide gets the full Delivr stack running on your machine so you can work on it. If you just want to ",[18,19,20],"em",{},"use"," Delivr, see ",[23,24,26],"a",{"href":25},"\u002Fdocs\u002Fself-hosting","Self-Hosting"," instead.",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,46,49,52],"ul",{},[37,38,39,45],"li",{},[23,40,44],{"href":41,"rel":42},"https:\u002F\u002Fbun.sh",[43],"nofollow","Bun"," 1.x",[37,47,48],{},"Git",[37,50,51],{},"An editor with TypeScript and Vue support — VS Code with the Vue (Volar) extension works well",[37,53,54,57],{},[18,55,56],{},"(Optional)"," a test mailbox with IMAP\u002FSMTP access",[29,59,61],{"id":60},"repositories","Repositories",[63,64,65,81],"table",{},[66,67,68],"thead",{},[69,70,71,75,78],"tr",{},[72,73,74],"th",{},"Repository",[72,76,77],{},"What it is",[72,79,80],{},"Dev port",[82,83,84,103,120],"tbody",{},[69,85,86,94,97],{},[87,88,89],"td",{},[23,90,93],{"href":91,"rel":92},"https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-API",[43],"Delivr-API",[87,95,96],{},"Bun + Hono backend",[87,98,99],{},[100,101,102],"code",{},"14123",[69,104,105,112,115],{},[87,106,107],{},[23,108,111],{"href":109,"rel":110},"https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-Web",[43],"Delivr-Web",[87,113,114],{},"Nuxt 4 web client & PWA",[87,116,117],{},[100,118,119],{},"14128",[69,121,122,129,132],{},[87,123,124],{},[23,125,128],{"href":126,"rel":127},"https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FWebsite",[43],"Website",[87,130,131],{},"This website and the docs",[87,133,134],{},[100,135,136],{},"14129",[14,138,139],{},"Clone them side by side:",[141,142,147],"pre",{"className":143,"code":144,"language":145,"meta":146,"style":146},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","mkdir delivr && cd delivr\ngit clone https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-API.git\ngit clone https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-Web.git\ngit clone https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FWebsite.git\n","bash","",[100,148,149,173,185,195],{"__ignoreMap":146},[150,151,154,158,162,166,170],"span",{"class":152,"line":153},"line",1,[150,155,157],{"class":156},"sBMFI","mkdir",[150,159,161],{"class":160},"sfazB"," delivr",[150,163,165],{"class":164},"sMK4o"," &&",[150,167,169],{"class":168},"s2Zo4"," cd",[150,171,172],{"class":160}," delivr\n",[150,174,176,179,182],{"class":152,"line":175},2,[150,177,178],{"class":156},"git",[150,180,181],{"class":160}," clone",[150,183,184],{"class":160}," https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-API.git\n",[150,186,188,190,192],{"class":152,"line":187},3,[150,189,178],{"class":156},[150,191,181],{"class":160},[150,193,194],{"class":160}," https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FDelivr-Web.git\n",[150,196,198,200,202],{"class":152,"line":197},4,[150,199,178],{"class":156},[150,201,181],{"class":160},[150,203,204],{"class":160}," https:\u002F\u002Fgithub.com\u002FDelivr-Project\u002FWebsite.git\n",[29,206,208],{"id":207},"run-the-api","Run the API",[141,210,212],{"className":143,"code":211,"language":145,"meta":146,"style":146},"cd Delivr-API\ncp example.env .env\n",[100,213,214,222],{"__ignoreMap":146},[150,215,216,219],{"class":152,"line":153},[150,217,218],{"class":168},"cd",[150,220,221],{"class":160}," Delivr-API\n",[150,223,224,227,230],{"class":152,"line":175},[150,225,226],{"class":156},"cp",[150,228,229],{"class":160}," example.env",[150,231,232],{"class":160}," .env\n",[14,234,235,236,239],{},"Edit ",[100,237,238],{},".env"," for local development:",[141,241,246],{"className":242,"code":243,"filename":244,"language":245,"meta":146,"style":146},"language-dotenv shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","DLA_APP_URL=http:\u002F\u002Flocalhost:14128\nDLA_ENCRYPTION_KEY=dev-only-key-at-least-32-characters-long\nDLA_LOG_LEVEL=debug\n","Delivr-API\u002F.env","dotenv",[100,247,248,253,258],{"__ignoreMap":146},[150,249,250],{"class":152,"line":153},[150,251,252],{},"DLA_APP_URL=http:\u002F\u002Flocalhost:14128\n",[150,254,255],{"class":152,"line":175},[150,256,257],{},"DLA_ENCRYPTION_KEY=dev-only-key-at-least-32-characters-long\n",[150,259,260],{"class":152,"line":187},[150,261,262],{},"DLA_LOG_LEVEL=debug\n",[14,264,265],{},"Then install, migrate, and start the watcher:",[141,267,269],{"className":143,"code":268,"language":145,"meta":146,"style":146},"bun install\nbun run db:sqlite:migrate\nbun run dev\n",[100,270,271,279,289],{"__ignoreMap":146},[150,272,273,276],{"class":152,"line":153},[150,274,275],{"class":156},"bun",[150,277,278],{"class":160}," install\n",[150,280,281,283,286],{"class":152,"line":175},[150,282,275],{"class":156},[150,284,285],{"class":160}," run",[150,287,288],{"class":160}," db:sqlite:migrate\n",[150,290,291,293,295],{"class":152,"line":187},[150,292,275],{"class":156},[150,294,285],{"class":160},[150,296,297],{"class":160}," dev\n",[14,299,300,301,304,305,308,309,312,313,315],{},"The API runs at ",[100,302,303],{},"http:\u002F\u002Flocalhost:14123",", with the interactive reference at ",[100,306,307],{},"http:\u002F\u002Flocalhost:14123\u002Fdocs\u002Fv1",". On the first start, the log prints a link to set the ",[100,310,311],{},"admin"," password — it points at the web client on port ",[100,314,119],{},".",[29,317,319],{"id":318},"run-the-web-client","Run the web client",[14,321,322],{},"In a second terminal:",[141,324,326],{"className":143,"code":325,"language":145,"meta":146,"style":146},"cd Delivr-Web\ncp example.env .env   # DELIVR_API_URL=http:\u002F\u002Flocalhost:14123\u002Fv1 is the default\nbun install\nbun run dev\n",[100,327,328,335,348,354],{"__ignoreMap":146},[150,329,330,332],{"class":152,"line":153},[150,331,218],{"class":168},[150,333,334],{"class":160}," Delivr-Web\n",[150,336,337,339,341,344],{"class":152,"line":175},[150,338,226],{"class":156},[150,340,229],{"class":160},[150,342,343],{"class":160}," .env",[150,345,347],{"class":346},"sHwdD","   # DELIVR_API_URL=http:\u002F\u002Flocalhost:14123\u002Fv1 is the default\n",[150,349,350,352],{"class":152,"line":187},[150,351,275],{"class":156},[150,353,278],{"class":160},[150,355,356,358,360],{"class":152,"line":197},[150,357,275],{"class":156},[150,359,285],{"class":160},[150,361,297],{"class":160},[14,363,364,365,368],{},"Open ",[100,366,367],{},"http:\u002F\u002Flocalhost:14128",", set the admin password with the link from the API log, and sign in.",[29,370,372],{"id":371},"tests-and-type-checking","Tests and type-checking",[14,374,375],{},"Both repositories run the same checks in CI on every push and pull request. Run them before you open a PR:",[141,377,379],{"className":143,"code":378,"language":145,"meta":146,"style":146},"bun run typecheck\nbun test\n",[100,380,381,390],{"__ignoreMap":146},[150,382,383,385,387],{"class":152,"line":153},[150,384,275],{"class":156},[150,386,285],{"class":160},[150,388,389],{"class":160}," typecheck\n",[150,391,392,394],{"class":152,"line":175},[150,393,275],{"class":156},[150,395,396],{"class":160}," test\n",[398,399,401],"h3",{"id":400},"api-tests","API tests",[14,403,404],{},"The API's suite is integration-heavy and exercises the real request paths:",[34,406,407,422,437],{},[37,408,409,412,413,416,417,421],{},[100,410,411],{},"bunfig.toml"," preloads ",[100,414,415],{},"tests\u002Fhelpers\u002Fpreload.ts",", which builds the app ",[418,419,420],"strong",{},"in-process"," without binding a port, so tests run fine while your dev server is up.",[37,423,424,425,428,429,432,433,436],{},"Mock IMAP servers listen on port ",[100,426,427],{},"11143"," (shared) and ",[100,430,431],{},"11144","–",[100,434,435],{},"11148"," (per-test). Make sure these ports are free.",[37,438,439],{},"Outgoing mail goes to mock SMTP servers and an in-memory transport — no mail ever leaves your machine.",[29,441,443],{"id":442},"the-generated-api-client","The generated API client",[14,445,446],{},"Delivr Web talks to the API through a type-safe client generated from the API's OpenAPI spec. Whenever API routes or models change:",[141,448,450],{"className":143,"code":449,"language":145,"meta":146,"style":146},"# With the API running on :14123\ncd Delivr-Web\nbun run api-client:generate\n",[100,451,452,457,463],{"__ignoreMap":146},[150,453,454],{"class":152,"line":153},[150,455,456],{"class":346},"# With the API running on :14123\n",[150,458,459,461],{"class":152,"line":175},[150,460,218],{"class":168},[150,462,334],{"class":160},[150,464,465,467,469],{"class":152,"line":187},[150,466,275],{"class":156},[150,468,285],{"class":160},[150,470,471],{"class":160}," api-client:generate\n",[14,473,474,475,478,479,482,483,315],{},"This rewrites the ",[100,476,477],{},"*.gen.ts"," files in ",[100,480,481],{},"app\u002Fapi-client\u002F",". Commit them, but ",[418,484,485],{},"never edit them by hand",[29,487,489],{"id":488},"changing-the-database","Changing the database",[14,491,492,493,498,499,502,503,506,507,510,511,510,514,517],{},"The API uses ",[23,494,497],{"href":495,"rel":496},"https:\u002F\u002Form.drizzle.team",[43],"Drizzle ORM"," with ",[418,500,501],{},"one schema file per SQL dialect"," in ",[100,504,505],{},"src\u002Fdb\u002Fschema\u002F"," (",[100,508,509],{},"sqlite.ts",", ",[100,512,513],{},"postgresql.ts",[100,515,516],{},"mysql.ts","). When you add a table or column:",[519,520,521,528,534],"ol",{},[37,522,523,524,527],{},"Make the change in ",[418,525,526],{},"all three"," schema files.",[37,529,530,531,315],{},"Generate the migration: ",[100,532,533],{},"bun run db:sqlite:generate",[37,535,536,537,540],{},"Review the generated SQL in ",[100,538,539],{},"drizzle\u002Fmigrations\u002Fsqlite\u002F"," and commit it.",[14,542,543,544,547,548,551,552,555],{},"For ",[418,545,546],{},"data migrations",", create a custom migration with ",[100,549,550],{},"bunx drizzle-kit generate --custom --name=\u003Cname> --config=drizzle\u002Fconfigs\u002Fdrizzle.sqlite.config.ts"," and write idempotent SQL (guard inserts with ",[100,553,554],{},"WHERE NOT EXISTS","). Remember that mail-account connection data is encrypted, so migrations can't read addresses or credentials from it.",[557,558,559],"tip",{},[14,560,561,564,565,568,569,572,573,576,577,315],{},[418,562,563],{},"New user preference?"," Preferences are schemaless rows in ",[100,566,567],{},"user_preferences",", validated by a Zod schema in ",[100,570,571],{},"src\u002Fapi\u002Futils\u002Fpreferences.ts",". Adding one needs no migration — add its schema to ",[100,574,575],{},"UserPreferences.schemas"," and its GET\u002FPUT routes under ",[100,578,579],{},"account\u002Fpreferences",[29,581,583],{"id":582},"adding-an-api-route","Adding an API route",[34,585,586,601,612,635,638],{},[37,587,588,589,592,593,596,597,600],{},"Routes live in ",[100,590,591],{},"src\u002Fapi\u002Fversions\u002Fv1\u002Froutes\u002F\u003Cresource>\u002F",", each with an ",[100,594,595],{},"index.ts"," (router) and a ",[100,598,599],{},"model.ts"," (Zod schemas).",[37,602,603,604,607,608,611],{},"Document every handler with the ",[100,605,606],{},"APIRouteSpec"," \u002F ",[100,609,610],{},"APIResponseSpec"," helpers so it appears in the OpenAPI spec.",[37,613,614,615,618,619,622,623,626,627,630,631,634],{},"New OpenAPI tags go into ",[100,616,617],{},"DOCS_TAGS"," ",[418,620,621],{},"and"," into the ",[100,624,625],{},"tags"," list and an ",[100,628,629],{},"x-tagGroups"," group in ",[100,632,633],{},"versions\u002Fv1\u002Findex.ts"," — otherwise they show up orphaned in the reference.",[37,636,637],{},"Wrap multi-step database writes in a transaction.",[37,639,640],{},"Never cache or persist mail content or attachments. Pool connections, not data.",[29,642,644],{"id":643},"building-release-artifacts","Building release artifacts",[646,647,648,680,727],"code-group",{},[141,649,652],{"className":143,"code":650,"filename":651,"language":145,"meta":146,"style":146},"cd Delivr-API\nbun run compile linux-x64-baseline --no-version-tag\n# → build\u002Fbin\u002Fdelivr-api-linux-x64-baseline\n","API binary",[100,653,654,660,675],{"__ignoreMap":146},[150,655,656,658],{"class":152,"line":153},[150,657,218],{"class":168},[150,659,221],{"class":160},[150,661,662,664,666,669,672],{"class":152,"line":175},[150,663,275],{"class":156},[150,665,285],{"class":160},[150,667,668],{"class":160}," compile",[150,670,671],{"class":160}," linux-x64-baseline",[150,673,674],{"class":160}," --no-version-tag\n",[150,676,677],{"class":152,"line":187},[150,678,679],{"class":346},"# → build\u002Fbin\u002Fdelivr-api-linux-x64-baseline\n",[141,681,684],{"className":143,"code":682,"filename":683,"language":145,"meta":146,"style":146},"cd Delivr-API\nbun run compile linux-x64-baseline --no-version-tag\ndocker build -f docker\u002FDockerfile -t delivr-api:dev .\n","API image",[100,685,686,692,704],{"__ignoreMap":146},[150,687,688,690],{"class":152,"line":153},[150,689,218],{"class":168},[150,691,221],{"class":160},[150,693,694,696,698,700,702],{"class":152,"line":175},[150,695,275],{"class":156},[150,697,285],{"class":160},[150,699,668],{"class":160},[150,701,671],{"class":160},[150,703,674],{"class":160},[150,705,706,709,712,715,718,721,724],{"class":152,"line":187},[150,707,708],{"class":156},"docker",[150,710,711],{"class":160}," build",[150,713,714],{"class":160}," -f",[150,716,717],{"class":160}," docker\u002FDockerfile",[150,719,720],{"class":160}," -t",[150,722,723],{"class":160}," delivr-api:dev",[150,725,726],{"class":160}," .\n",[141,728,731],{"className":143,"code":729,"filename":730,"language":145,"meta":146,"style":146},"cd Delivr-Web\nbun run build\ndocker build -f docker\u002FDockerfile -t delivr-web:dev .\n","Web image",[100,732,733,739,748],{"__ignoreMap":146},[150,734,735,737],{"class":152,"line":153},[150,736,218],{"class":168},[150,738,334],{"class":160},[150,740,741,743,745],{"class":152,"line":175},[150,742,275],{"class":156},[150,744,285],{"class":160},[150,746,747],{"class":160}," build\n",[150,749,750,752,754,756,758,760,763],{"class":152,"line":187},[150,751,708],{"class":156},[150,753,711],{"class":160},[150,755,714],{"class":160},[150,757,717],{"class":160},[150,759,720],{"class":160},[150,761,762],{"class":160}," delivr-web:dev",[150,764,726],{"class":160},[29,766,768],{"id":767},"working-on-the-docs","Working on the docs",[14,770,771,772,777,778,783],{},"This website is a static ",[23,773,776],{"href":774,"rel":775},"https:\u002F\u002Fnuxt.com",[43],"Nuxt 4"," site with ",[23,779,782],{"href":780,"rel":781},"https:\u002F\u002Fcontent.nuxt.com",[43],"Nuxt Content",":",[141,785,787],{"className":143,"code":786,"language":145,"meta":146,"style":146},"cd Website\nbun install\nbun run dev   # http:\u002F\u002Flocalhost:14129\n",[100,788,789,796,802],{"__ignoreMap":146},[150,790,791,793],{"class":152,"line":153},[150,792,218],{"class":168},[150,794,795],{"class":160}," Website\n",[150,797,798,800],{"class":152,"line":175},[150,799,275],{"class":156},[150,801,278],{"class":160},[150,803,804,806,808,811],{"class":152,"line":187},[150,805,275],{"class":156},[150,807,285],{"class":160},[150,809,810],{"class":160}," dev",[150,812,813],{"class":346},"   # http:\u002F\u002Flocalhost:14129\n",[34,815,816,826,852],{},[37,817,818,819,822,823,315],{},"Docs pages are Markdown files in ",[100,820,821],{},"content\u002Fdocs\u002F",". Add new pages to the sidebar in ",[100,824,825],{},"app\u002Fdata\u002Fdocs.ts",[37,827,828,829,510,832,510,835,510,838,510,841,510,844,847,848,851],{},"You can use components such as ",[100,830,831],{},"::note",[100,833,834],{},"::tip",[100,836,837],{},"::warning",[100,839,840],{},"::steps",[100,842,843],{},"::tabs",[100,845,846],{},"::code-group",", and ",[100,849,850],{},"::card-group"," in Markdown.",[37,853,854,855,858,859,862,863,866],{},"Format with ",[100,856,857],{},"bun run format"," and check with ",[100,860,861],{},"bun run check"," before committing. ",[100,864,865],{},"bun run generate"," builds the static site.",[29,868,870],{"id":869},"next-steps","Next steps",[14,872,873,874,878],{},"Read the ",[23,875,877],{"href":876},"\u002Fdocs\u002Fcontributing","Contributing guide"," for the pull-request workflow and conventions.",[880,881,882],"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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}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}",{"title":146,"searchDepth":175,"depth":175,"links":884},[885,886,887,888,889,892,893,894,895,896,897],{"id":31,"depth":175,"text":32},{"id":60,"depth":175,"text":61},{"id":207,"depth":175,"text":208},{"id":318,"depth":175,"text":319},{"id":371,"depth":175,"text":372,"children":890},[891],{"id":400,"depth":187,"text":401},{"id":442,"depth":175,"text":443},{"id":488,"depth":175,"text":489},{"id":582,"depth":175,"text":583},{"id":643,"depth":175,"text":644},{"id":767,"depth":175,"text":768},{"id":869,"depth":175,"text":870},"Run Delivr API and Delivr Web locally, run the test suites, regenerate the API client, change the database schema, and work on the docs.","md",{},{"title":5},"\u002Fdocs\u002Fdevelopment",{"title":5,"description":898},"docs\u002Fdevelopment","N-G-SQjX83Azb5OyAfVVAdBDe_nmSdwDJPYCqK37xbc",1791069427749]