From bf730a31e53f70d06160c1db1ec35ce2bfcbd0be Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:20:23 +0200 Subject: [PATCH] Deploy website - based on 9ccbc8d43bcaa2fb83cadf0c68f1fc26b4026ade --- .DS_Store | Bin 0 -> 8196 bytes 404.html | 8 ++--- ...rison-7ba8be56668425ec89309e370f0a872c.png | Bin 0 -> 44846 bytes ...ac00a.ffabdd23.js => 045ac00a.a575dec6.js} | 2 +- ...8e6cc.522f1c8d.js => 0608e6cc.92d93b83.js} | 2 +- assets/js/080b96ba.5aa6e358.js | 1 - assets/js/080b96ba.d3c0e68c.js | 1 + assets/js/0a3b3433.8f99b470.js | 1 + assets/js/0a3b3433.995f1851.js | 1 - ...3c7f5.51b96fc9.js => 0b5d4ec2.c9186086.js} | 2 +- ...100e0.7e236724.js => 0c3100e0.273b90d8.js} | 2 +- assets/js/0dc22d83.04d6b122.js | 1 - assets/js/0dc22d83.b48ce449.js | 1 + ...d3d16.e7455a32.js => 104d3d16.744e974a.js} | 2 +- assets/js/1072d334.2ba88f37.js | 1 + assets/js/1072d334.fd7b4cd5.js | 1 - assets/js/115a88b0.071e72cb.js | 1 + assets/js/115a88b0.ad0d4dea.js | 1 - assets/js/130db68e.e8b0f639.js | 1 - ...e4e2b.0668ccf9.js => 16ee4e2b.fac7e241.js} | 2 +- ...4cfa7.3519ca07.js => 182d49f9.e6877c1d.js} | 2 +- ...8083a.cdecf66e.js => 18c8083a.7724c00f.js} | 2 +- ...36e0e.ec0f29f8.js => 18f36e0e.61ddb577.js} | 2 +- ...8f921.2376c63a.js => 1978f921.f537130f.js} | 2 +- ...5f360.4dca31cd.js => 1ca5f360.93fb0291.js} | 2 +- ...d49f9.9e316e40.js => 1d94cfa7.167bf44b.js} | 2 +- assets/js/21ffb138.153ce797.js | 1 + assets/js/21ffb138.ad0b4c5a.js | 1 - ...d4ec2.23fafd50.js => 2313c7f5.96c42c77.js} | 2 +- assets/js/2841d226.1793a284.js | 1 + assets/js/2841d226.2781d773.js | 1 - ...a9bbb.960412c2.js => 2db37094.ba31d64e.js} | 2 +- assets/js/362b8996.ec3e252f.js | 1 + ...3bec0.26a03fb9.js => 3663bec0.534aa0de.js} | 2 +- ...477ec.f05f09f2.js => 366477ec.75651f9a.js} | 2 +- ...71570.f05c5efd.js => 36871570.7faad1c9.js} | 2 +- ...08406.35cd4e72.js => 36908406.a72132f0.js} | 2 +- ...bd8f8.283a952f.js => 369bd8f8.d8a9114c.js} | 2 +- assets/js/3d5d9ec4.5a028c57.js | 1 - assets/js/3d5d9ec4.b7906377.js | 1 + ...12a28.f3038cfa.js => 40412a28.c5a3ba0f.js} | 2 +- ...488a0.660652b4.js => 407488a0.3e846094.js} | 2 +- assets/js/42dc7ad7.2f361f02.js | 1 - assets/js/42dc7ad7.7f519f78.js | 1 + assets/js/47147745.1cfbfcab.js | 1 + ...48124.9dccfd0c.js => 51748124.beb3d7cc.js} | 2 +- ...4577f.3ff2eb5e.js => 5344577f.17bdd5e6.js} | 2 +- ...fbeff.cf340044.js => 545fbeff.1640c6af.js} | 2 +- ...29dca.af54e48a.js => 57629dca.279eea1f.js} | 2 +- ...b0fb4.5bbb510a.js => 5aab0fb4.b50c8ebd.js} | 2 +- ...cee6e.b5134b49.js => 5bbcee6e.9fd88bee.js} | 2 +- ...288af.60eb4bed.js => 62b288af.4bbf5daf.js} | 2 +- ...9051e.309516ea.js => 6659051e.64d39603.js} | 2 +- ...f9382.d04125c2.js => 691f9382.6678f067.js} | 2 +- ...ffe59.3db38ab9.js => 69bffe59.ac9c7e70.js} | 2 +- ...43024.b1a692e6.js => 6a143024.d73a70fd.js} | 2 +- ...d3135.856b18b4.js => 6d4d3135.6ee1819e.js} | 2 +- ...57cc8.ffc032fc.js => 71257cc8.1e15f84d.js} | 2 +- assets/js/72b8013f.56ad5eaf.js | 1 - assets/js/72b8013f.b410d35e.js | 1 + ...fb94d.82404fa5.js => 738fb94d.c2d072a0.js} | 2 +- assets/js/740fc5e3.07106fe7.js | 1 + assets/js/740fc5e3.e85bf9f5.js | 1 - ...5d095.ad0408e4.js => 76d5d095.569a3480.js} | 2 +- ...23114.7a2dee96.js => 77e23114.744c67cc.js} | 2 +- ...9227a.d3fdc063.js => 7849227a.1433b4a2.js} | 2 +- ...564d9.cfb2d653.js => 7d3564d9.2d3abede.js} | 2 +- ...f7aa2.1b7085b9.js => 7d3f7aa2.b13fa518.js} | 2 +- ...5e522.90e53c5a.js => 7f65e522.eaf075c2.js} | 2 +- assets/js/814f3328.0fb0ef78.js | 1 + assets/js/814f3328.f943a663.js | 1 - ...51dd4.93cac1a9.js => 81b51dd4.34cbd4d7.js} | 2 +- ...56314.e72bca5c.js => 85f56314.46904007.js} | 2 +- ...22b11.79ecbec6.js => 95622b11.c3070b14.js} | 2 +- ...098b1.215dd29d.js => 962098b1.75ad1c36.js} | 2 +- assets/js/965820cb.06a7ef2e.js | 1 + assets/js/965820cb.b96ae6aa.js | 1 - ...d2545.63a758a9.js => 965d2545.b2882ac3.js} | 2 +- ...f7180.a1c59da0.js => 986f7180.57514f5d.js} | 2 +- ...35dfb.20d5d82b.js => 9fe35dfb.b65793ae.js} | 2 +- assets/js/a4cc42e0.e11e5bee.js | 1 + assets/js/a7023ddc.b3918f34.js | 1 - assets/js/a7023ddc.c74db29c.js | 1 + ...2f4c4.5110896d.js => ab42f4c4.6eb3e51b.js} | 2 +- ...243b8.02170088.js => ae0243b8.67daed9c.js} | 2 +- ...ac3fa.625ff0d7.js => b08ac3fa.5eaeb77e.js} | 2 +- ...8b350.4de3027c.js => b1ce8d31.4c92fbf7.js} | 2 +- ...675dd.65192f76.js => b2b675dd.5cbeabba.js} | 2 +- assets/js/b2f554cd.4a258478.js | 1 + assets/js/b2f554cd.53597846.js | 1 - ...22e59.6d4e02c0.js => b4022e59.22135c57.js} | 2 +- ...3559c.2a26565a.js => b7d3559c.538000d5.js} | 2 +- ...fdb29.dbd849bd.js => bf8fdb29.b78e787d.js} | 2 +- ...bc25e.c33a0063.js => c30bc25e.64c27a69.js} | 2 +- ...65978.f3d87951.js => c7b65978.aba62046.js} | 2 +- ...7e918.3172c403.js => cf47e918.8d7409b5.js} | 2 +- ...b1d91.b01c4d3b.js => cfbb1d91.3f5a9b05.js} | 2 +- ...6be7b.35dd6abd.js => cfefff41.d3477434.js} | 2 +- ...bfdd2.139005c4.js => d08bfdd2.0510e127.js} | 2 +- assets/js/d732aeea.f3083950.js | 1 + ...f8c6b.43a390ee.js => d7ef8c6b.9acd98fb.js} | 2 +- ...adc90.fa89f408.js => d98adc90.4c5b3d97.js} | 2 +- ...197c7.b46dd0ec.js => e9e197c7.0d4f87fd.js} | 2 +- ...60dde.0ba96449.js => e9f60dde.c373f4ac.js} | 2 +- assets/js/ea1507ec.24bd23b3.js | 1 - assets/js/ea1507ec.49fb9dfe.js | 1 + ...48131.74094a9f.js => eaf48131.a898168d.js} | 2 +- assets/js/ebb83163.2beff60c.js | 1 + assets/js/ebb83163.98b43e4c.js | 1 - ...c04f0.d6979dd1.js => f18c04f0.1a6ed070.js} | 2 +- ...afe58.535981ec.js => f27afe58.d371c08b.js} | 2 +- assets/js/f50647f0.2ce4fc45.js | 1 + ...36422.7e324016.js => f5b36422.e8ee5364.js} | 2 +- ...2263b.2ee9894e.js => f802263b.b9bfe94a.js} | 2 +- assets/js/main.1478af8d.js | 2 -- assets/js/main.879c69d6.js | 2 ++ ...CENSE.txt => main.879c69d6.js.LICENSE.txt} | 0 assets/js/runtime~main.75a6c34e.js | 1 - assets/js/runtime~main.d213a97b.js | 1 + blog.html | 10 +++--- blog/2019/09/01/hello-wasp.html | 10 +++--- blog/2021/02/23/journey-to-ycombinator.html | 10 +++--- blog/2021/03/02/wasp-alpha.html | 10 +++--- blog/2021/04/29/discord-bot-introduction.html | 10 +++--- blog/2021/09/01/haskell-forall-tutorial.html | 10 +++--- blog/2021/11/21/seed-round.html | 10 +++--- blog/2021/11/22/fundraising-learnings.html | 10 +++--- blog/2021/12/02/waspello.html | 10 +++--- blog/2021/12/21/shayne-intro.html | 10 +++--- blog/2022/01/27/waspleau.html | 10 +++--- blog/2022/05/31/filip-intro.html | 10 +++--- blog/2022/06/01/gitpod-hackathon-guide.html | 10 +++--- .../2022/06/15/jobs-feature-announcement.html | 10 +++--- .../ML-code-gen-vs-coding-by-hand-future.html | 10 +++--- ...ate-why-your-startup-is-worth-joining.html | 10 +++--- ...ow-and-why-i-got-started-with-haskell.html | 10 +++--- ...w-to-get-started-with-haskell-in-2022.html | 10 +++--- blog/2022/09/05/dev-excuses-app-tutrial.html | 10 +++--- blog/2022/09/29/journey-to-1000-gh-stars.html | 10 +++--- .../2022/10/28/farnance-hackathon-winner.html | 10 +++--- .../2022/11/15/auth-feature-announcement.html | 10 +++--- .../16/alpha-testing-program-post-mortem.html | 10 +++--- .../11/16/tailwind-feature-announcement.html | 10 +++--- blog/2022/11/17/hacktoberfest-wrap-up.html | 10 +++--- blog/2022/11/26/erlis-amicus-usecase.html | 10 +++--- blog/2022/11/26/michael-curry-usecase.html | 10 +++--- blog/2022/11/26/wasp-beta-launch-week.html | 10 +++--- blog/2022/11/28/why-we-chose-prisma.html | 10 +++--- blog/2022/11/29/permissions-in-web-apps.html | 10 +++--- .../29/typescript-feature-announcement.html | 10 +++--- blog/2022/11/29/wasp-beta.html | 10 +++--- ...ptimistic-update-feature-announcement.html | 10 +++--- blog/2022/12/01/beta-ide-improvements.html | 10 +++--- blog/2022/12/08/fast-fullstack-chatgpt.html | 10 +++--- blog/2023/01/11/betathon-review.html | 10 +++--- blog/2023/01/18/wasp-beta-update-dec.html | 10 +++--- blog/2023/01/31/wasp-beta-launch-review.html | 10 +++--- blog/2023/02/02/no-best-framework.html | 10 +++--- .../02/14/amicus-indiehacker-interview.html | 10 +++--- .../21/junior-developer-misconceptions.html | 10 +++--- blog/2023/03/02/wasp-beta-update-feb.html | 10 +++--- ...truths-junior-developers-need-to-hear.html | 10 +++--- ...ing-a-full-stack-app-supabase-vs-wasp.html | 10 +++--- ...ew-react-docs-pretend-spas-dont-exist.html | 10 +++--- blog/2023/04/11/wasp-launch-week-two.html | 10 +++--- blog/2023/04/12/auth-ui.html | 10 +++--- blog/2023/04/13/db-start-and-seed.html | 10 +++--- .../04/17/How-I-Built-CoverLetterGPT.html | 10 +++--- blog/2023/04/27/wasp-hackathon-two.html | 10 +++--- blog/2023/05/19/hackathon-2-review.html | 10 +++--- blog/2023/06/07/wasp-beta-update-may-23.html | 10 +++--- blog/2023/06/22/wasp-launch-week-three.html | 10 +++--- ...uild-your-own-twitter-agent-langchain.html | 10 +++--- .../06/28/what-can-you-build-with-wasp.html | 10 +++--- blog/2023/06/29/new-wasp-lsp.html | 10 +++--- blog/2023/06/30/tutorial-jam.html | 10 +++--- blog/2023/07/10/gpt-web-app-generator.html | 10 +++--- .../how-we-built-gpt-web-app-generator.html | 10 +++--- blog/2023/08/01/smol-ai-vs-wasp-ai.html | 10 +++--- ...oting-app-websockets-react-typescript.html | 10 +++--- ...ents-generate-better-web-apps-with-ai.html | 10 +++--- ...rator-how-to-use-openai-function-call.html | 10 +++--- .../contributing-open-source-land-a-job.html | 10 +++--- ...n-importance-of-naming-in-programming.html | 10 +++--- blog/2023/10/13/wasp-launch-week-four.html | 10 +++--- .../guide-windows-development-wasp-wsl.html | 10 +++--- blog/2023/12/05/writing-rfcs.html | 10 +++--- blog/2024/01/23/wasp-launch-week-five.html | 10 +++--- ...free-open-source-starter-react-nodejs.html | 10 +++--- ...ets-you-visualize-react-node-app-code.html | 10 +++--- .../05/22/how-to-get-a-web-dev-job-2024.html | 10 +++--- ...-dont-have-laravel-for-javascript-yet.html | 10 +++--- .../03/building-selling-saas-in-5-months.html | 10 +++--- blog/2024/07/15/wasp-launch-week-six.html | 10 +++--- ...h-with-lucia-to-your-react-nextjs-app.html | 32 ++++++++++++++++++ blog/archive.html | 10 +++--- blog/atom.xml | 20 ++++++++++- blog/rss.xml | 16 ++++++++- blog/tags.html | 10 +++--- blog/tags/acquire.html | 10 +++--- blog/tags/agent.html | 10 +++--- blog/tags/ai.html | 10 +++--- blog/tags/auth.html | 10 +++--- blog/tags/boilerplate.html | 10 +++--- blog/tags/career.html | 10 +++--- blog/tags/chakra.html | 10 +++--- blog/tags/chatgpt.html | 10 +++--- blog/tags/clean-code.html | 10 +++--- blog/tags/css.html | 10 +++--- blog/tags/database.html | 10 +++--- blog/tags/discord.html | 10 +++--- blog/tags/express.html | 10 +++--- blog/tags/feature.html | 10 +++--- blog/tags/framework.html | 10 +++--- blog/tags/full-stack.html | 10 +++--- blog/tags/fullstack.html | 10 +++--- blog/tags/function-calling.html | 10 +++--- blog/tags/generate.html | 10 +++--- blog/tags/github.html | 10 +++--- blog/tags/gitpod.html | 10 +++--- blog/tags/gpt.html | 10 +++--- blog/tags/hack.html | 10 +++--- blog/tags/hackathon.html | 10 +++--- blog/tags/hacktoberfest.html | 10 +++--- blog/tags/haskell.html | 10 +++--- blog/tags/hiring.html | 10 +++--- blog/tags/indie-hacker.html | 10 +++--- blog/tags/interview.html | 10 +++--- blog/tags/javascript.html | 10 +++--- blog/tags/job.html | 10 +++--- blog/tags/jobs.html | 10 +++--- blog/tags/junior-developers.html | 10 +++--- blog/tags/langchain.html | 10 +++--- blog/tags/language.html | 10 +++--- blog/tags/laravel.html | 10 +++--- blog/tags/launch-week.html | 10 +++--- blog/tags/mage.html | 10 +++--- blog/tags/marketing.html | 10 +++--- blog/tags/meme.html | 10 +++--- blog/tags/ml.html | 10 +++--- blog/tags/new-hire.html | 10 +++--- blog/tags/nextjs.html | 31 +++++++++++++++++ blog/tags/node.html | 10 +++--- blog/tags/nodejs.html | 10 +++--- blog/tags/open-source.html | 10 +++--- blog/tags/openai.html | 10 +++--- blog/tags/optimistic.html | 10 +++--- blog/tags/pern.html | 10 +++--- blog/tags/prd.html | 10 +++--- blog/tags/prisma.html | 10 +++--- blog/tags/product-requirement.html | 10 +++--- blog/tags/product-update.html | 10 +++--- blog/tags/programming.html | 10 +++--- blog/tags/rails.html | 10 +++--- blog/tags/react.html | 12 +++---- blog/tags/real-time.html | 10 +++--- blog/tags/reddit.html | 10 +++--- blog/tags/saa-s.html | 10 +++--- blog/tags/saas.html | 10 +++--- blog/tags/showcase.html | 10 +++--- blog/tags/solopreneur.html | 10 +++--- blog/tags/startup.html | 10 +++--- blog/tags/startups.html | 10 +++--- blog/tags/state-of-js.html | 10 +++--- blog/tags/stripe.html | 10 +++--- blog/tags/supabase.html | 10 +++--- blog/tags/tech-career.html | 10 +++--- blog/tags/tech.html | 12 +++---- blog/tags/tutorial.html | 12 +++---- blog/tags/typescript.html | 10 +++--- blog/tags/update.html | 10 +++--- blog/tags/updates.html | 10 +++--- blog/tags/wasp-ai.html | 10 +++--- blog/tags/wasp.html | 10 +++--- blog/tags/web-dev.html | 10 +++--- blog/tags/web-development.html | 10 +++--- blog/tags/webdev.html | 12 +++---- blog/tags/websockets.html | 10 +++--- blog/tags/windows.html | 10 +++--- blog/tags/wsl.html | 10 +++--- docs.html | 8 ++--- docs/0.11.8.html | 8 ++--- docs/0.11.8/advanced/apis.html | 8 ++--- docs/0.11.8/advanced/deployment/cli.html | 8 ++--- docs/0.11.8/advanced/deployment/manually.html | 8 ++--- docs/0.11.8/advanced/deployment/overview.html | 8 ++--- docs/0.11.8/advanced/email.html | 8 ++--- docs/0.11.8/advanced/jobs.html | 8 ++--- docs/0.11.8/advanced/links.html | 8 ++--- docs/0.11.8/advanced/middleware-config.html | 8 ++--- docs/0.11.8/advanced/web-sockets.html | 8 ++--- docs/0.11.8/auth/email.html | 8 ++--- docs/0.11.8/auth/overview.html | 8 ++--- docs/0.11.8/auth/social-auth/github.html | 8 ++--- docs/0.11.8/auth/social-auth/google.html | 8 ++--- docs/0.11.8/auth/social-auth/overview.html | 8 ++--- docs/0.11.8/auth/ui.html | 8 ++--- docs/0.11.8/auth/username-and-pass.html | 8 ++--- docs/0.11.8/contact.html | 8 ++--- docs/0.11.8/contributing.html | 8 ++--- docs/0.11.8/data-model/backends.html | 8 ++--- docs/0.11.8/data-model/crud.html | 8 ++--- docs/0.11.8/data-model/entities.html | 8 ++--- .../0.11.8/data-model/operations/actions.html | 8 ++--- .../data-model/operations/overview.html | 8 ++--- .../0.11.8/data-model/operations/queries.html | 8 ++--- docs/0.11.8/editor-setup.html | 8 ++--- docs/0.11.8/general/cli.html | 8 ++--- docs/0.11.8/general/language.html | 8 ++--- docs/0.11.8/project/client-config.html | 8 ++--- docs/0.11.8/project/css-frameworks.html | 8 ++--- docs/0.11.8/project/custom-vite-config.html | 8 ++--- docs/0.11.8/project/customizing-app.html | 8 ++--- docs/0.11.8/project/dependencies.html | 8 ++--- docs/0.11.8/project/env-vars.html | 8 ++--- docs/0.11.8/project/server-config.html | 8 ++--- docs/0.11.8/project/starter-templates.html | 8 ++--- docs/0.11.8/project/static-assets.html | 8 ++--- docs/0.11.8/project/testing.html | 8 ++--- docs/0.11.8/quick-start.html | 8 ++--- docs/0.11.8/telemetry.html | 8 ++--- docs/0.11.8/tutorial/actions.html | 8 ++--- docs/0.11.8/tutorial/auth.html | 8 ++--- docs/0.11.8/tutorial/create.html | 8 ++--- docs/0.11.8/tutorial/entities.html | 8 ++--- docs/0.11.8/tutorial/pages.html | 8 ++--- docs/0.11.8/tutorial/project-structure.html | 8 ++--- docs/0.11.8/tutorial/queries.html | 8 ++--- docs/0.11.8/vision.html | 8 ++--- docs/0.11.8/writingguide.html | 8 ++--- docs/0.12.0.html | 8 ++--- docs/0.12.0/advanced/apis.html | 8 ++--- docs/0.12.0/advanced/deployment/cli.html | 8 ++--- docs/0.12.0/advanced/deployment/manually.html | 8 ++--- docs/0.12.0/advanced/deployment/overview.html | 8 ++--- docs/0.12.0/advanced/email.html | 8 ++--- docs/0.12.0/advanced/jobs.html | 8 ++--- docs/0.12.0/advanced/links.html | 8 ++--- docs/0.12.0/advanced/middleware-config.html | 8 ++--- docs/0.12.0/advanced/web-sockets.html | 8 ++--- docs/0.12.0/auth/email.html | 8 ++--- docs/0.12.0/auth/entities.html | 8 ++--- docs/0.12.0/auth/overview.html | 8 ++--- docs/0.12.0/auth/social-auth/github.html | 8 ++--- docs/0.12.0/auth/social-auth/google.html | 8 ++--- docs/0.12.0/auth/social-auth/overview.html | 8 ++--- docs/0.12.0/auth/ui.html | 8 ++--- docs/0.12.0/auth/username-and-pass.html | 8 ++--- docs/0.12.0/contact.html | 8 ++--- docs/0.12.0/contributing.html | 8 ++--- docs/0.12.0/data-model/backends.html | 8 ++--- docs/0.12.0/data-model/crud.html | 8 ++--- docs/0.12.0/data-model/entities.html | 8 ++--- .../0.12.0/data-model/operations/actions.html | 8 ++--- .../data-model/operations/overview.html | 8 ++--- .../0.12.0/data-model/operations/queries.html | 8 ++--- docs/0.12.0/editor-setup.html | 8 ++--- docs/0.12.0/general/cli.html | 8 ++--- docs/0.12.0/general/language.html | 8 ++--- docs/0.12.0/migrate-from-0-11-to-0-12.html | 8 ++--- docs/0.12.0/project/client-config.html | 8 ++--- docs/0.12.0/project/css-frameworks.html | 8 ++--- docs/0.12.0/project/custom-vite-config.html | 8 ++--- docs/0.12.0/project/customizing-app.html | 8 ++--- docs/0.12.0/project/dependencies.html | 8 ++--- docs/0.12.0/project/env-vars.html | 8 ++--- docs/0.12.0/project/server-config.html | 8 ++--- docs/0.12.0/project/starter-templates.html | 8 ++--- docs/0.12.0/project/static-assets.html | 8 ++--- docs/0.12.0/project/testing.html | 8 ++--- docs/0.12.0/quick-start.html | 8 ++--- docs/0.12.0/telemetry.html | 8 ++--- docs/0.12.0/tutorial/actions.html | 8 ++--- docs/0.12.0/tutorial/auth.html | 8 ++--- docs/0.12.0/tutorial/create.html | 8 ++--- docs/0.12.0/tutorial/entities.html | 8 ++--- docs/0.12.0/tutorial/pages.html | 8 ++--- docs/0.12.0/tutorial/project-structure.html | 8 ++--- docs/0.12.0/tutorial/queries.html | 8 ++--- docs/0.12.0/vision.html | 8 ++--- docs/0.12.0/wasp-ai/creating-new-app.html | 8 ++--- .../wasp-ai/developing-existing-app.html | 8 ++--- docs/0.12.0/writingguide.html | 8 ++--- docs/0.13.0.html | 8 ++--- .../0.13.0/advanced/accessing-app-config.html | 8 ++--- docs/0.13.0/advanced/apis.html | 8 ++--- docs/0.13.0/advanced/deployment/cli.html | 8 ++--- docs/0.13.0/advanced/deployment/manually.html | 8 ++--- docs/0.13.0/advanced/deployment/overview.html | 8 ++--- docs/0.13.0/advanced/email.html | 8 ++--- docs/0.13.0/advanced/jobs.html | 8 ++--- docs/0.13.0/advanced/links.html | 8 ++--- docs/0.13.0/advanced/middleware-config.html | 8 ++--- docs/0.13.0/advanced/web-sockets.html | 8 ++--- docs/0.13.0/auth/email.html | 8 ++--- docs/0.13.0/auth/entities.html | 8 ++--- docs/0.13.0/auth/overview.html | 8 ++--- docs/0.13.0/auth/social-auth/github.html | 8 ++--- docs/0.13.0/auth/social-auth/google.html | 8 ++--- docs/0.13.0/auth/social-auth/keycloak.html | 8 ++--- docs/0.13.0/auth/social-auth/overview.html | 8 ++--- docs/0.13.0/auth/ui.html | 8 ++--- docs/0.13.0/auth/username-and-pass.html | 8 ++--- docs/0.13.0/contact.html | 8 ++--- docs/0.13.0/contributing.html | 8 ++--- docs/0.13.0/data-model/backends.html | 8 ++--- docs/0.13.0/data-model/crud.html | 8 ++--- docs/0.13.0/data-model/entities.html | 8 ++--- .../0.13.0/data-model/operations/actions.html | 8 ++--- .../data-model/operations/overview.html | 8 ++--- .../0.13.0/data-model/operations/queries.html | 8 ++--- docs/0.13.0/editor-setup.html | 8 ++--- docs/0.13.0/general/cli.html | 8 ++--- docs/0.13.0/general/language.html | 8 ++--- docs/0.13.0/migrate-from-0-11-to-0-12.html | 8 ++--- docs/0.13.0/migrate-from-0-12-to-0-13.html | 8 ++--- docs/0.13.0/project/client-config.html | 8 ++--- docs/0.13.0/project/css-frameworks.html | 8 ++--- docs/0.13.0/project/custom-vite-config.html | 8 ++--- docs/0.13.0/project/customizing-app.html | 8 ++--- docs/0.13.0/project/dependencies.html | 8 ++--- docs/0.13.0/project/env-vars.html | 8 ++--- docs/0.13.0/project/server-config.html | 8 ++--- docs/0.13.0/project/starter-templates.html | 8 ++--- docs/0.13.0/project/static-assets.html | 8 ++--- docs/0.13.0/project/testing.html | 8 ++--- docs/0.13.0/quick-start.html | 8 ++--- docs/0.13.0/telemetry.html | 8 ++--- docs/0.13.0/tutorial/actions.html | 8 ++--- docs/0.13.0/tutorial/auth.html | 8 ++--- docs/0.13.0/tutorial/create.html | 8 ++--- docs/0.13.0/tutorial/entities.html | 8 ++--- docs/0.13.0/tutorial/pages.html | 8 ++--- docs/0.13.0/tutorial/project-structure.html | 8 ++--- docs/0.13.0/tutorial/queries.html | 8 ++--- docs/0.13.0/vision.html | 8 ++--- docs/0.13.0/wasp-ai/creating-new-app.html | 8 ++--- .../wasp-ai/developing-existing-app.html | 8 ++--- docs/0.13.0/writingguide.html | 8 ++--- docs/advanced/accessing-app-config.html | 8 ++--- docs/advanced/apis.html | 8 ++--- docs/advanced/deployment/cli.html | 8 ++--- docs/advanced/deployment/manually.html | 8 ++--- docs/advanced/deployment/overview.html | 8 ++--- docs/advanced/email.html | 8 ++--- docs/advanced/jobs.html | 8 ++--- docs/advanced/links.html | 8 ++--- docs/advanced/middleware-config.html | 8 ++--- docs/advanced/web-sockets.html | 8 ++--- docs/auth/auth-hooks.html | 8 ++--- docs/auth/email.html | 8 ++--- docs/auth/entities.html | 8 ++--- docs/auth/overview.html | 8 ++--- docs/auth/social-auth/discord.html | 8 ++--- docs/auth/social-auth/github.html | 8 ++--- docs/auth/social-auth/google.html | 8 ++--- docs/auth/social-auth/keycloak.html | 8 ++--- docs/auth/social-auth/overview.html | 8 ++--- docs/auth/ui.html | 8 ++--- docs/auth/username-and-pass.html | 8 ++--- docs/contact.html | 8 ++--- docs/contributing.html | 8 ++--- docs/data-model/backends.html | 8 ++--- docs/data-model/crud.html | 8 ++--- docs/data-model/entities.html | 8 ++--- docs/data-model/operations/actions.html | 8 ++--- docs/data-model/operations/overview.html | 8 ++--- docs/data-model/operations/queries.html | 8 ++--- docs/data-model/prisma-file.html | 8 ++--- docs/editor-setup.html | 8 ++--- docs/general/cli.html | 8 ++--- docs/general/language.html | 8 ++--- docs/general/typescript.html | 8 ++--- docs/migrate-from-0-11-to-0-12.html | 8 ++--- docs/migrate-from-0-12-to-0-13.html | 8 ++--- docs/migrate-from-0-13-to-0-14.html | 8 ++--- docs/project/client-config.html | 8 ++--- docs/project/css-frameworks.html | 8 ++--- docs/project/custom-vite-config.html | 8 ++--- docs/project/customizing-app.html | 8 ++--- docs/project/dependencies.html | 8 ++--- docs/project/env-vars.html | 8 ++--- docs/project/server-config.html | 8 ++--- docs/project/starter-templates.html | 8 ++--- docs/project/static-assets.html | 8 ++--- docs/project/testing.html | 8 ++--- docs/quick-start.html | 8 ++--- docs/telemetry.html | 8 ++--- docs/tutorial/actions.html | 8 ++--- docs/tutorial/auth.html | 8 ++--- docs/tutorial/create.html | 8 ++--- docs/tutorial/entities.html | 8 ++--- docs/tutorial/pages.html | 8 ++--- docs/tutorial/project-structure.html | 8 ++--- docs/tutorial/queries.html | 8 ++--- docs/vision.html | 8 ++--- docs/wasp-ai/creating-new-app.html | 8 ++--- docs/wasp-ai/developing-existing-app.html | 8 ++--- docs/writingguide.html | 8 ++--- img/.DS_Store | Bin 0 -> 10244 bytes img/lua-auth/comparison.png | Bin 0 -> 44846 bytes img/lua-auth/lucia-auth-banner.png | Bin 0 -> 49895 bytes img/websockets-app/.DS_Store | Bin 0 -> 6148 bytes index.html | 8 ++--- search.html | 8 ++--- sitemap.xml | 2 +- 506 files changed, 1871 insertions(+), 1772 deletions(-) create mode 100644 .DS_Store create mode 100644 assets/images/comparison-7ba8be56668425ec89309e370f0a872c.png rename assets/js/{045ac00a.ffabdd23.js => 045ac00a.a575dec6.js} (98%) rename assets/js/{0608e6cc.522f1c8d.js => 0608e6cc.92d93b83.js} (98%) delete mode 100644 assets/js/080b96ba.5aa6e358.js create mode 100644 assets/js/080b96ba.d3c0e68c.js create mode 100644 assets/js/0a3b3433.8f99b470.js delete mode 100644 assets/js/0a3b3433.995f1851.js rename assets/js/{2313c7f5.51b96fc9.js => 0b5d4ec2.c9186086.js} (80%) rename assets/js/{0c3100e0.7e236724.js => 0c3100e0.273b90d8.js} (98%) delete mode 100644 assets/js/0dc22d83.04d6b122.js create mode 100644 assets/js/0dc22d83.b48ce449.js rename assets/js/{104d3d16.e7455a32.js => 104d3d16.744e974a.js} (97%) create mode 100644 assets/js/1072d334.2ba88f37.js delete mode 100644 assets/js/1072d334.fd7b4cd5.js create mode 100644 assets/js/115a88b0.071e72cb.js delete mode 100644 assets/js/115a88b0.ad0d4dea.js delete mode 100644 assets/js/130db68e.e8b0f639.js rename assets/js/{16ee4e2b.0668ccf9.js => 16ee4e2b.fac7e241.js} (98%) rename assets/js/{1d94cfa7.3519ca07.js => 182d49f9.e6877c1d.js} (93%) rename assets/js/{18c8083a.cdecf66e.js => 18c8083a.7724c00f.js} (98%) rename assets/js/{18f36e0e.ec0f29f8.js => 18f36e0e.61ddb577.js} (90%) rename assets/js/{1978f921.2376c63a.js => 1978f921.f537130f.js} (98%) rename assets/js/{1ca5f360.4dca31cd.js => 1ca5f360.93fb0291.js} (99%) rename assets/js/{182d49f9.9e316e40.js => 1d94cfa7.167bf44b.js} (94%) create mode 100644 assets/js/21ffb138.153ce797.js delete mode 100644 assets/js/21ffb138.ad0b4c5a.js rename assets/js/{0b5d4ec2.23fafd50.js => 2313c7f5.96c42c77.js} (80%) create mode 100644 assets/js/2841d226.1793a284.js delete mode 100644 assets/js/2841d226.2781d773.js rename assets/js/{f1da9bbb.960412c2.js => 2db37094.ba31d64e.js} (62%) create mode 100644 assets/js/362b8996.ec3e252f.js rename assets/js/{3663bec0.26a03fb9.js => 3663bec0.534aa0de.js} (99%) rename assets/js/{366477ec.f05f09f2.js => 366477ec.75651f9a.js} (99%) rename assets/js/{36871570.f05c5efd.js => 36871570.7faad1c9.js} (58%) rename assets/js/{36908406.35cd4e72.js => 36908406.a72132f0.js} (90%) rename assets/js/{369bd8f8.283a952f.js => 369bd8f8.d8a9114c.js} (51%) delete mode 100644 assets/js/3d5d9ec4.5a028c57.js create mode 100644 assets/js/3d5d9ec4.b7906377.js rename assets/js/{40412a28.f3038cfa.js => 40412a28.c5a3ba0f.js} (98%) rename assets/js/{407488a0.660652b4.js => 407488a0.3e846094.js} (51%) delete mode 100644 assets/js/42dc7ad7.2f361f02.js create mode 100644 assets/js/42dc7ad7.7f519f78.js create mode 100644 assets/js/47147745.1cfbfcab.js rename assets/js/{51748124.9dccfd0c.js => 51748124.beb3d7cc.js} (95%) rename assets/js/{5344577f.3ff2eb5e.js => 5344577f.17bdd5e6.js} (99%) rename assets/js/{545fbeff.cf340044.js => 545fbeff.1640c6af.js} (98%) rename assets/js/{57629dca.af54e48a.js => 57629dca.279eea1f.js} (99%) rename assets/js/{5aab0fb4.5bbb510a.js => 5aab0fb4.b50c8ebd.js} (50%) rename assets/js/{5bbcee6e.b5134b49.js => 5bbcee6e.9fd88bee.js} (78%) rename assets/js/{62b288af.60eb4bed.js => 62b288af.4bbf5daf.js} (90%) rename assets/js/{6659051e.309516ea.js => 6659051e.64d39603.js} (51%) rename assets/js/{691f9382.d04125c2.js => 691f9382.6678f067.js} (96%) rename assets/js/{69bffe59.3db38ab9.js => 69bffe59.ac9c7e70.js} (99%) rename assets/js/{6a143024.b1a692e6.js => 6a143024.d73a70fd.js} (94%) rename assets/js/{6d4d3135.856b18b4.js => 6d4d3135.6ee1819e.js} (50%) rename assets/js/{71257cc8.ffc032fc.js => 71257cc8.1e15f84d.js} (97%) delete mode 100644 assets/js/72b8013f.56ad5eaf.js create mode 100644 assets/js/72b8013f.b410d35e.js rename assets/js/{738fb94d.82404fa5.js => 738fb94d.c2d072a0.js} (98%) create mode 100644 assets/js/740fc5e3.07106fe7.js delete mode 100644 assets/js/740fc5e3.e85bf9f5.js rename assets/js/{76d5d095.ad0408e4.js => 76d5d095.569a3480.js} (51%) rename assets/js/{77e23114.7a2dee96.js => 77e23114.744c67cc.js} (93%) rename assets/js/{7849227a.d3fdc063.js => 7849227a.1433b4a2.js} (51%) rename assets/js/{7d3564d9.cfb2d653.js => 7d3564d9.2d3abede.js} (96%) rename assets/js/{7d3f7aa2.1b7085b9.js => 7d3f7aa2.b13fa518.js} (98%) rename assets/js/{7f65e522.90e53c5a.js => 7f65e522.eaf075c2.js} (94%) create mode 100644 assets/js/814f3328.0fb0ef78.js delete mode 100644 assets/js/814f3328.f943a663.js rename assets/js/{81b51dd4.93cac1a9.js => 81b51dd4.34cbd4d7.js} (97%) rename assets/js/{85f56314.e72bca5c.js => 85f56314.46904007.js} (99%) rename assets/js/{95622b11.79ecbec6.js => 95622b11.c3070b14.js} (80%) rename assets/js/{962098b1.215dd29d.js => 962098b1.75ad1c36.js} (97%) create mode 100644 assets/js/965820cb.06a7ef2e.js delete mode 100644 assets/js/965820cb.b96ae6aa.js rename assets/js/{965d2545.63a758a9.js => 965d2545.b2882ac3.js} (65%) rename assets/js/{986f7180.a1c59da0.js => 986f7180.57514f5d.js} (68%) rename assets/js/{9fe35dfb.20d5d82b.js => 9fe35dfb.b65793ae.js} (99%) create mode 100644 assets/js/a4cc42e0.e11e5bee.js delete mode 100644 assets/js/a7023ddc.b3918f34.js create mode 100644 assets/js/a7023ddc.c74db29c.js rename assets/js/{ab42f4c4.5110896d.js => ab42f4c4.6eb3e51b.js} (99%) rename assets/js/{ae0243b8.02170088.js => ae0243b8.67daed9c.js} (90%) rename assets/js/{b08ac3fa.625ff0d7.js => b08ac3fa.5eaeb77e.js} (87%) rename assets/js/{7d28b350.4de3027c.js => b1ce8d31.4c92fbf7.js} (62%) rename assets/js/{b2b675dd.65192f76.js => b2b675dd.5cbeabba.js} (73%) create mode 100644 assets/js/b2f554cd.4a258478.js delete mode 100644 assets/js/b2f554cd.53597846.js rename assets/js/{b4022e59.6d4e02c0.js => b4022e59.22135c57.js} (52%) rename assets/js/{b7d3559c.2a26565a.js => b7d3559c.538000d5.js} (97%) rename assets/js/{bf8fdb29.dbd849bd.js => bf8fdb29.b78e787d.js} (99%) rename assets/js/{c30bc25e.c33a0063.js => c30bc25e.64c27a69.js} (98%) rename assets/js/{c7b65978.f3d87951.js => c7b65978.aba62046.js} (96%) rename assets/js/{cf47e918.3172c403.js => cf47e918.8d7409b5.js} (98%) rename assets/js/{cfbb1d91.b01c4d3b.js => cfbb1d91.3f5a9b05.js} (67%) rename assets/js/{eb26be7b.35dd6abd.js => cfefff41.d3477434.js} (62%) rename assets/js/{d08bfdd2.139005c4.js => d08bfdd2.0510e127.js} (98%) create mode 100644 assets/js/d732aeea.f3083950.js rename assets/js/{d7ef8c6b.43a390ee.js => d7ef8c6b.9acd98fb.js} (95%) rename assets/js/{d98adc90.fa89f408.js => d98adc90.4c5b3d97.js} (90%) rename assets/js/{e9e197c7.b46dd0ec.js => e9e197c7.0d4f87fd.js} (95%) rename assets/js/{e9f60dde.0ba96449.js => e9f60dde.c373f4ac.js} (99%) delete mode 100644 assets/js/ea1507ec.24bd23b3.js create mode 100644 assets/js/ea1507ec.49fb9dfe.js rename assets/js/{eaf48131.74094a9f.js => eaf48131.a898168d.js} (96%) create mode 100644 assets/js/ebb83163.2beff60c.js delete mode 100644 assets/js/ebb83163.98b43e4c.js rename assets/js/{f18c04f0.d6979dd1.js => f18c04f0.1a6ed070.js} (98%) rename assets/js/{f27afe58.535981ec.js => f27afe58.d371c08b.js} (99%) create mode 100644 assets/js/f50647f0.2ce4fc45.js rename assets/js/{f5b36422.7e324016.js => f5b36422.e8ee5364.js} (98%) rename assets/js/{f802263b.2ee9894e.js => f802263b.b9bfe94a.js} (82%) delete mode 100644 assets/js/main.1478af8d.js create mode 100644 assets/js/main.879c69d6.js rename assets/js/{main.1478af8d.js.LICENSE.txt => main.879c69d6.js.LICENSE.txt} (100%) delete mode 100644 assets/js/runtime~main.75a6c34e.js create mode 100644 assets/js/runtime~main.d213a97b.js create mode 100644 blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app.html create mode 100644 blog/tags/nextjs.html create mode 100644 img/.DS_Store create mode 100644 img/lua-auth/comparison.png create mode 100644 img/lua-auth/lucia-auth-banner.png create mode 100644 img/websockets-app/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f804f8c9669a836737f5a5da58d8b205352479da GIT binary patch literal 8196 zcmeHMYit!o6rOKe=&oJpl!q*Gfh$!|ys6`R1IN zGqdO1@62TOGRDwaH0v2N7-K3u0(m7>*D0c3Jl7N@(v%Z|?3q8BbG#f$pX5fb867L) z2*eSHBM?U*jzAoNn;`;pW{aZU;M|wJaUDk>j=+qJfY=|B^axBxI4LpQI;aY-0EFTS z5Eg24(I8Aln2vB#BK1&#G$oX#h^`or=A=(Xs=Q+7jJDcmVt??ngpv`lGtnFHb(7-k=8nUO?c1>@*)hoKDAKGSQhpMUC5H}7T zYG`UsB^%c^Po$EEnj7j;$u*5l6BC-cu%>qXuA%+Ti0d7Z4j|eD*qmt&J%7a;#~*8x zUn9!a=SHPB=Stpaw^ZtLjGo?}K7F9i&U2as44) zozFV?qLU4*7RM2TNmu3P3!dZb^n5$8J-5Fvunq*JiKcY*4ftLmXbUGB;=JfvyDXZ^ z9<$nMMw)7#SgmVROyl=h1)s1cT_>H>j?G<^xc$!6wQHImXzSR%^Gp@5o;QDit`AWB z+`!&zTZP@@wiyifXZ^h8nzp;Icg*(foMUyn*%7N6=QQoiJYHR8EH;*?yhiOS=IqCd zcJM?v*V1K%zQ?2&+2NUDHj^J$wXkQ!GQ$`YJ#7VxYExB1^~zO--tAKmcs3e%PfFLj z_tOu|eOgMqU|qd#>~^v_%i*Rb&F*W`jovZo9p4ry*R9u$zPvXwLazALNJZ*RX~TF# zth_zz2VJ9B7G<6!{3m4gxO}(4!i<&hORF&x>U7a zOxa?ZmKna}L%W0px7{(DSWQ8f>Mf zmwD_kdx0HgAF)r_3HA*;%Pz2A*&pm8y97W%8Fb7+6*v}S1!_@`1~g&=I?#!&$e<4c z7{ne}cnl6k;9?vTcoLI1j3amvFXI)wilcZ7Z{r=ji;wXozQQSdjnnuMKjAzs;CK9u ze{e~uP*g=z<|_%MR;gDSltyKZvQgQhWR!0C_DU(C2B&(ZSR_Z=10_`Sc~5!&`U#cj z=W8^9g%;krC@J!4?Z(#26qKAT(e!Hahm4=XfspZGicO>H8DsHnOA<@< z<)u8Fo}arZRjdQ5;5)C7qgUv6sFW&vmK0a%cdL{ne6|!*iF;K_ORj{XUY}^xDEWAq z6jO=&HI+jO>+}s8r6Dhu;wHUC6FUHBp`2(>Y*8td_ zO!{hw`~T(}fB&CB5s6)mBQWzKfb#ZCdn@^Uqa9J)wUhKbN{=XF+@!?NgsSj5P8eRt m2`~R)NbMvI6*lPzCnb`G%K!f%K#R}+`1}u<7ZPxWR{sKddsmPE literal 0 HcmV?d00001 diff --git a/404.html b/404.html index 401254995f..9656c799d0 100644 --- a/404.html +++ b/404.html @@ -19,13 +19,13 @@ - - + +

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- - + + \ No newline at end of file diff --git a/assets/images/comparison-7ba8be56668425ec89309e370f0a872c.png b/assets/images/comparison-7ba8be56668425ec89309e370f0a872c.png new file mode 100644 index 0000000000000000000000000000000000000000..22ea75aa64628e5163437c21608e12e0915fcfcf GIT binary patch literal 44846 zcma%iQ;;S+u=d!tZQHhO;~g72=8kRec*pka*tTuko_WvrU;pRgq$;UOF1pg4?#`1Z zNtB9`G$I@x90&*qqO6RB8VCp&;J;c91ODI0%`v(t2naZcih_pZ*W()SWdr!S34Gc7 zd|3T>Sp9rh`+45j-56M!Yu{P#1HNqmU$=gq*S??D-|v=zKl_eeHq7EYJaWQZvO>J_ zA`Bv2rVhrhH%m9C)5p8RCwrq$R}0^d>la58&c1eRl6-s$qWp?tOkzAA_bb4U?YyD{ z4Kq!EvuSTnIq+@!Y=0~u!b9Ii|KVZ|__Z4p=}F4P$R#7lD#6Px#a~;S4Se4@*dEdX zXmdylaLNdNJ*)zsH-KOJVR63n!koe?5&}wM4UM@*c7}q=;&?g5UTLC({Y&9s8- zf=c4G^*P0*$<+L8|5^%wAG`btq9UqN?d`>@i=EuE!Wn;}IHUxCpZi^1r9j}p@L)}4 zb!KjUTxxd2`N8ml+5sw^3=ri(D>BfogF1&>e4Dk zYSP;Ba=MCYCK{0m{*s!q3i`@QhALaDy^NyV-XX5mZWex_?sgtlpZBY}mb$UYK|tUU zpS;NOLPu0$;P^-#5O}_^+->Os2#N6l0&iZf7lFXj{=Ul3`<1WPt*hgyvdZ-N>6VYX z72wl4B_GS@)7tyP&im~$9t#aR9r-|K>1cmdZf3ZRg&rIn?0-wFC?|@Fij0qkO;1ND zCc>kkA|oj-P+OkP&PuPZqokuL@BPaFnpG1-Pa7a~#5CTulxFQgtpXA*$g8dNtkxIw%6t*H^}J)$3qZ zIaRV>n>rx$pF#h(!rJm6p%dYHZeV4^Q?~g%?~`t-lkn!#CKU2KeXGP%BJAt*H8!&F z6X?{4uBzJ!XbM(apN3L98!w#d zm#rl5+2}Mb^9ByJ@Uu7ZGp`!>POn)+N7qzOTOM9fmO!-Iw->q=O-|gJYtBiu(q%zc znD0@CRZsN05m586s%IebZc7wZb(FS3RLtZG(r1@PJR`p8I26D43dOKzOWYv`F;JJD z&odK6P>p};{2;J-y2TiZj`~>u9me!FqdDH=fG}gOi7n845Q<$J-nflS#wdiBXFts1 zP)A<}d3lvPLTzu#K@8BkfsuZvF39Zz( z0Reu2s$4O!9sr7>?-jwBfyq26eA+lXwIFmdjTgKAp0XPLO+B0*cFU*@?&Vd+2&vn(bJ zVM!vfXlqccieF+HM^FHIilm3c+SrjzJea4re7s#JnM0Y{{Ij z7@@ZY8{2*iVJ@>I`ddX_G;;IsuB5X)@OBomK-vcLpK@Mf3TEo&SDG;gsVaFZ70nDI z9Xa*Vtdhpe%M8rStJPCkbg&F}U0%N&aPcxuzNUunYMuJfSGgW5W(AbD(=D}hkzmse z^!>RCGt3#%ug5D(ARIR?w$8i_Od42*(%2+Doe%X>e$;qZ_Le4ePoveA|D9nsb^cbb zQ_}NiS0{2X58wdsjs<1m)u~-^8Nobe4}du}@xMsPum_#JwRkI`Q(t?e_%M#lV@8EZ z?ewfV{9tOE+3d856?@hsO~ogc<2q589lAHP%7p$RqBzW)Dm|%K4$44>*lOx(gAU1yXxTxjvw17T--G%}0UH&EO`675Rpll^egvD1Ah;y^<^`bU^_CN+ zxiOfH4Owpokf5P0I6R8YrLQIYs|~gvjku_91b-xytg7X6_+u#iF*hFyhK2k}lLuSjB zRBEdH9TP*Yd__;NL`x+u&;Y&J!dJYI0(jBl#iR!+5Le)6n#6FRsLD!mbA9|!h#yya zS_h$6p-W_nCiBq2C;&=#G){jg-7>>QJ|07(Dhl3@-5;z7Hw&w^QnMk7utVPALbpQq zKOH~*kbv4>2)cp(kCkXx8lo!R5Lx373I`AxLS)`NdCz=a2D#?PLx?@d|E)S-vAcfh zS>eMhDY|g2xtZ5agX))ifm^NDVDKq^|NBL;Ok{13B61z!>rL~5R}Y_2gAG*rveNyE z5DS1p}=Ejg0n?@`A zDEMEp8@blTw5qz;Mw_${fy(o|1fQlAEU9g`W{Xm2_Vq+SRAf;x{va4ixkU#%3UT5B z;h1{Xdnys~{%3{V{O&3RHP$^0Mi;#$<(!yKOEMxnqLvuv7aLOqGubZvr`^W!oCCJG zR1rayqmPWiYjc5he+f-rH}Y&l6Q*{(1q5ts`eH&y8H9IQ&Lp}KQ3C+_@1V{7)60C|Nh#UR3e zF|%4~U<9+oXqyPT;CrAv&G<@@Yy1 z@a9;|gE=U-AU!NhG;)XYdb64iwHDBo%$sgHW9wJt)q-HM-^3w|tS4~NzvL1#lk@Gu zW3?@+EJE2rV7tguPI$mKF^GVidIyWC-U+vi@nQs!es%<-JX6YvOw+7opV5}6FGtn7 z2>q^HnJY@y7x5wZr0_pk|D1!8s1y}T3Uo+;nVhn1Dj`X8 zpJn#;iue4}2hrPL52=L#*Yz9eP%1IW?c?v3a76Y<k~+)>NCBux3LM;r`M;E zO(J5psu|EfS;J;rNxWCh`F23rwx@OtubDN?UVX*3L|dvR#IY5ZJ#_a z>z3q-AmZi;6wbDLip}MbFEIKI`; z>Nj!5rWUnZds-q`Kz~X7W)a4D>0^t44}cnD?F0#q86PRDXWN{Bjtk>DVhJ6}n?m9K zjezsOPsWG2NFf8x{`I312 z02pS*ZhF2DoGLCQWfxpHEv5eT6I%$nwD?1UCwmJ5cPWn|HuC}aLuJG_V4$<%M1k-~ zP_>MaT?TRwtbRvHVWvLd4voElH=}cw$qTPGm`$Z^v0D0-_^x$Yd>{!fo&bqeCn_)z z4OijKoRCw&%ozd{0{pX}%szj<09DaSb-=4!Ass};es_(HI8>nrj3l{Mlt=1z1ejFp;BL1$#^l`^i1p49NVScz~= zo-Y#u#65nKqE`#Xg%gA5I$}$0qzFb?h*DYkvR&9`N=_=&<(w(;4E2cR z6M|Y%HktV1o|Qp#Cg-wUYc^(vp=3mfkhn`Jzp)sp|n>wK^6a}o&# z*AqbD2!lJnA`okuWLqZ7I+&9CH=k+q!4WP?*0-biJboU5&4e}H>)E1GJ4_g$OAC$5 zn<&y-x$ey-uG>IiG6qM(fjiBw$#X-A{D6wrgKAxRcw0j^rH_z1a3pNC8NJ9I9&5to zSMhAGJ5_M-i%z*U615W*(C3^)T_tEYIh8V9@&JIB6eh#X2|;L|LARwlbQ;TS%+63P z4N0(1$Nd#TUH#qR!Z9=09LAz3)>MUF%ff3r0X3>3qouqH#x}?=xf}1B{SOHca4}bb za~JbpK)~D6@ucd{%)GsxUBcyc5QG1Rh~9s&Vyq8Bjxl6r1zG)9V?C+kJs{?ZcX%{; zszy#IGK(rDEgR0m;4;GaW;Q<|uk9}pWVZ;|9x86kKk~rHGs*gdIT4hLx0! zS!9);>qR)z@SOU9fo*|nk%aXAX5c}`RG;<7X}r@}#m%~)CYja6^0da*cY@2JY=Ix_ zK?kyHCPoRZ5c7dM<$>RklS~c+ihwPDT~%6-Z^RN^;Z6zAW@}Fl@iL1tLD((gJ#*j? zY77D|v*hMn^n!)otu>;-=`nL5#a-HVCz ziO%FG%r*ww#uvsd4J`i)amP~`=%DYmzMdU@z#I{f0040qIDN@K(UmYmlUy|AY8;YB z;kjfY9d2DcnkLq{v}p~+!~d)29RBh|1?ZLHg$Y=irweCbGdL}a`-=Ia$^i zyrAb~aOCX7bIUF)z6Hf5lEz&2^2WUtvW$ClPDl)0#UBept~iDKn*kb^bmICA6#WL9 zA16!ryBS-9#hifOjdkw+hyiY~J_O$#T%g|l&{2&0@oL2SiEb?Dy}vPJv&+K$q{;dDaODm$btufiV0&4dK;;Xw;IZhK3=b!IvT@ zAoIVx4>k1`7+xR{t(}54|N1Jg%>L6Y?9+9y|4aFVpUKSK%~i1ALd(u2Ykxu&?YqUO zo2SalKe)D0OeAt16rbMY$$63J28CDBN}dq>S_owflzA04z&VR5J zMnv%z65=Y}i>9ka_1UWo;k*;+?1sq>XOSa)$A9+C>bJdZ>mNL}V?)@i%M@ho*hX|U z*wENF&7UDUQUx&Nu!M-K&rPfYtvrNR)jy4m7pKdxVrI9o6ag6jNoTRJxQ^D|`-cz~ z);D3^m@GptG=GV~^G$sGW3Q-by=C-rKuc$jFUvNRR)mjF&@P}fNj?Tc(rbithht>e zRiNWL$+(xEXgYB=PDwp$rG2Wh(kXivNBSRH{gLG<8XpXRWs3?K^%Dq3kmHy;q+HMm zUG_(t+@QGmPNKieWB(Cd!3sXO4zbCW_{Y^RMt|DJ?^KKNtJ!{8-^82$57T|I_)oA& zOG!>V&B`}!I9ik)3BEX@Tn5P^-xZ(Fp$ot$YPG{x4u7b6cBgN_V`|AlT!}lxUXYbw zfdZoUi72oYoDhE`ku<7#%j?53#N0t>_%zCSKfSD&N*6*`GVWFDUx5}Ij*(!zCBg<- zV|aK8k^1GYkf*B7_j#649|6>KbgommC;`ELs{g+2jrYii@@K8g|CQ|AJI3fz)CON2 zliJsZY%{l~9)VD0{cCF<8NAp_bFNB=$2V-wd=FsLM@fI_8B{Q2+fv|-a)FwBf`m{X zWs3vBq3t;E+dJQUy{@w~=8m!CW;D{|m@)XtMC6F!aH#=V=lbsWIG(ghDLXEmKp5C| zDDd<1Xz2dFUJQPe$}R$F|9NuG1cXWv#BY|$bg7Kf$N&2W;7|Hts0yNwaiMi|KT_f66~E(XuHC%mh_M8#s~_6G>&jeuL4hE9U$Z#kiQXwp+lzTX}Hz zV|OsI(OM)Y&Hv5((5(~Wu;#>9xW|wGd$#ddlo80BP@qf_eszi9zwAHu`QYEjU8rHy z>;l8K3hnnmIX1*I=h&Jw5_VHN25YF8zfI?RW!9=HqLoepO?+*vxZ| z#_alsInl+odsb#h`bq3mk26j2UD@+BgM0PG6FcE(03QJ$z3nUZmM%7rnf=OT2slUbHM=;t3k*CbfDf^dU0mvS>+j^1;Z z+hHBPtf%T4v~#m^{F7Z&s<|Y78X!b@un0Q;87jF6wZe5kBcTe$`)r-?y{(AeS_b&oUi7#v<=;W^aNcx`NIK5_XS^cVLApN-^4NG)K zl1=5Mvlqu@rj-;I>WRM>T*UA|N^TDI2>h6al|L=%=_>+1+QzhpPR*AdwW3v{(Mp^K z7WK{Va^CNd+2GLts>jJ4d4lFDH5YQ&x$BaPmF6558W_{O)4|^)^&5njHXb4HV??_1 z`Gso9khnin0-b-wPnfjsI>XD7F^9F3s^ZXvF#U1aQUtzb4%0E2@QxiO)=m)O-fZX@ ziQw%t-3Uj0t{6MuU*>6Cx*X1zD2+lFqb)J~I3d|UoUX@!JjT&WgMw9Yix)@UqI;Oq zvb?_tEi%*A)IuZTt3^3dyWNUfk#ffE$B^#0(Fr+q6Ze1H{x#DdH|=v}CAB5qn4>@kU?GnVr-u02KJL9% zJUUP9i(Yfvxb2YbAT=66ihsWt&-(c=?ZJ)Vyi3Ln9rOojJUUCfi)-n{E|w4LLMYS& z8;tU3D<5IHglw5p?w8v9`FV(p=oF@Efj$2ouklfmCYO!JC?I{jRo#DU;MC6u}Mt_m;whO zAN^`e@%>uhNxw_E1J(EnC-$5$oW%N*c)H)uEJoGwkU@x(RZmfKt(ArV{X{F3nD-ti zo$nY2gOb_JP99KxR+|h#DB&c3VX^A4d`25jSo_FM^^W_S;p$9K5P_I3>gOjk;q*yw z6^`a!Px_=T<&N@(E13#qba(L1{f~O7zF6zCG!R|#^<(ap_Z1=?qy$k z!LPC<1V^MS3&K%A@D4ZV^5L($>LbH9FhJi|;c67FdAH!>w3Wr;W(fXHeGh<#8HhB6 z$*=TBlE%4Tl?xHGU@Hv;z*+o(wLGeRg-+|t)Uw#UR@4q^b!hxgr_=@ z1E%gp4ph%zmHJFMUF)sGYnOB*x*=0ITy=2@OTS?$nuC$`}$+EfIDmmoY!$ zHj5yickS+ifi9;QYjHa?7RADAvoO^4!fbMh965eQo~X|+E^2L%J!j8>$7K>jc0$!f zW&eIo$NXkfd}N(V%f6>Y`Q6G2@>j8OWP7jx66|jaLHN>gh2+#LB8UD|*crMe*vKDI z4*qbkQAH}9AbXuG#frUlS0rc!GulX=>S3u_IGV5*e< z>e22lPvyT(pzHcDxE|Y zj>ldwqhJ@j4!;f2J}&uzFs}cRgQylgW8_4lM-?kyig;}4$I#JqrYnP21EyqO=(g#K zt0GcyYc2*xVeG*pP;2;ki1Ba0-c3;`g_0j5!p87Y9=~UXVFs*^5iskj&mWIZQsZC(T=TDkxT)=$sXag#%gInL6UL{(6!Q;*7l?<$}zQakhT2 zO&IUvTPx8f8W>L6+2s+1fxCKdN>!<=1>7=&jB`lO0z%|jHv(?~7$}~i_fbk;5Qpd1 zT;}o*eKG!?AC&=-0|67^huyu4(+_Z7}G2f zkeO*%b&lw8vo*1%^{Ye8xbj;FA_H+;9w&t2u7GIwG>qB zZo5?GAu}w)A#GRwKzQ>2)nk0fb)cR9IYDOG5|b`YbJ5@& zhg-@iFv4JyDyq~BZKJT?%k{_GwaHF>kyKipgT@_f^ZJ`~g`7-LhtG2|=UH+0l2O6G z2-|&pUx0F^dakj=w7jyf*3$3#cpLcrmBA=(z~rdoH^L_c@J|%3j5C?5c5-#Yn|E4Z zfSi0`di`SHrp;j^eLR4{7+13z4t^r8s|&7faUlK}65rv@S(;_@b3N!mI4Vo6mn6nlgMkqNa@{t!f2Hd9M~lGnq9`f)Hh|5?advFAhX& z?2CXm5Q+UEZbovrq#+Td0>rz~bw_&KERlAWDhFi~x9-oH5eqU~6dVvM>kwHr-cQ_e z|5i}xAW}JRd6v}3XKDSbfRu#lZiz0k&FUN=#ws&~$2@7TCm?Y$>!rQeW$F`3$fVqRwdZv&><4`LThC zjWbOb;qYLKhEcsQMy0j{-^oW3RQh&xnaqrzHV{N*ZMJX}+! z0pS+pc2j^DEvqP41V&L~nWJay6A_0kUa&Z_IkP-(Jrd#)aL<*vPu=J+$(d*~v{TWv5$OrA`uB?pmL=6-5&3YThThujOv3E9s;lcf1>M- zM-qo)EX692zv5}lb)t)MypG2Zdk6tuiwDB+Y()-8-$`&$>rp(6t(ze%)(1uiS#y#? zOQeSm$PC_DF){pn0K&Ei1YvqZXk4Nm6)u2x#hq+R4=v{@s(0XuHG+*^B7Ip#3-O>@ zi|*w*f*e;pWOGmSbQrPPdLnFF2tuT*;LB#cy!s>f>D!O1P?X z-XuuoBzM6&w=6X@C8*NOE#1Zs7NDrh@DQ;qQ7&fDVlx`s2s#V+d^dYDK2^f6#eKY%0CYGB_E?63{=(H+r1+JZxr2K6|qc)_7c2va+-kqGQJG% z{l*j>{NUfW>1tM{K&CLVvw94Mv|e-Xw+~scOWl2qD2U1XO!?7IWR*;L`$Ip}ScWkc zKGoS;V_ew7_L?G2C3SD>Uf$bO(YR47rm@m$h{F~~B#3)iGpS&TGug@FH!*^n8_sZL z=WQ)3-ukAQ2rADIiLH(%s9Sfzrp^C^G4gvdwDArsrj^Ei(pRq+@_af%Oy?^Nn({6$ zzq_dtWn5?J$&&=!UDAmxl={x3^@&$wy`Rx!>`rKi{1p-~E*jzdmLh%<5UB;aJVf5$ zj{NiARab<(oQg^LM^s+lA5OrG^=S^#A7wjI)e)fJ?^rAD?8PjJRsQ2UI}&zb1xj=I zi8z}vx+rNQe`+T}Jy1FdJpt!_0iK@%$wB5N;96XIws}3~taJZarF75_qw#;Xh*EqF ze`f3mrvA?INuFzKb&y6MBl;0Dh$_6xvBKb+yxO4(Rh zCT!C@Gr?b#@ZVu$w&~P5nLCxS!5PVyFhohG<%KJ|e>b>#nFj~vAb^-*%F=dU6$M&< zfa0bM$3NQ#jxaOwvTc>ekiJ4#5HR0^nF9jKn;%f6r-*H2z;Ei4 zaLZp7t1>pJupb86|303W0Vipx`*KM_jy?MHT)a-on)~Z^@Ogim>-?&EZgG4Ab*|aDkiW zD3kSJhZ4HiwU5?ecn02%oz_nx|FBgcC*TGEgL4$i)7U$gGcIHdME3 zt=;%WYMFJU(NMM=`kvB+|W^OR#?6%k_@tz}+?mK3ASPdO}L3;41GQYVtvPE~3`71m5q9^98@XJyxEw zdeL=U@0`Y=9Ub&_1iB@+`W(idvmsTef9UEkV}e;_YSwv~u`VV)RaW(z(jt!rGn^vNN(W)z2 zOg{#dL`KODE-M1M?mX5je2n23cHDjR$E-_thz=4{r5hsd7d{)>L?!!TIo*L-(Kz2_ zFl+=B*B*;xOy8h!q!`RPRSkikwqxZk{@Qu480 z(Md0Jn7V8dltsB3%J*c%)^WD-W`uWIBY|FH4^z~Gd4m`Ld8?DD5Fk;u`yTl~X0t)3 zw*Fz7hQER5!E7+mwg(@CzXt*}SiD3pGtY`jt@w(I5C0fUQb%u{+W>WXhlgip`W3;0 zc|=c1PfU(ow4;+01Yu)ehp?o&`$NT}2>cyE_;!^LYY|+fA21 zen;Y@&2MDx}wQ!wdes$%=hC>WKtN@`2(cS(QLn=la@n2PUzq>S(7blN&J zqT5Fx2;4v>r|EOJi^hy@Oy{(jZOnDVLFz59hWKwhxceS+)<}}WrH-OaQK`tAc?-Kt z>6V7%McM|s1ka+d3xnQLcH3=1rR`u>F)LL#Hegp@nV-WTF5(XqpwFPEa*uj!V??;0 za;(G7USJ3EWiIYS9^}>1#O75LCY)!MVm-{B;ZwRF{Y|@3%rd zk$$AT=0a0fhPwB69O%9{4TB?{pS#0TTMdmJ<2KFO#tue9WUA zep*~O3owU+zYCzetIWNFIYOrF_3bBTpjz0Q(VGv&IJdPLnB5^l2ni#+zJiYTSjug9 z7Je}TbTm9a)yxzSD?ehSe^;6zzZ`Wc=)%NC@bRhSPVl#7M$~`^IC(NbHeh{CK?0gO ze(}JwK>}lR_hL5dB>zpoCwSh``#avl`wD$J-mEId~o&^kq`_w$^I!kT9HdIUm zE5RB5{hnI<^bn#EsVFQ^LnPfa-%rhgd!i1gN-0678PCuc7HaS(Q~5`IQ@y>3-)Gh#p4%gplql`9ZS%$GJ&2jgMJ zc_NP~#(vUL_XlL9Kiokx*MW)Q_ST=m-*Z@RyE8`?W0|Gu$JhqylhXGOaiAkjP#Qnu z#A@s%pHC;R_}@x3t3uL!^PDDy@`0P*v*=r+r5aY{zv+~F%@tG$Rzw1w`(_g@DsnUp zl0et#ujBhyGU5&R0PO9(-*3hTH>n5-_d5Yb{=rTXtrBGGI63|hyLfHO4>!|I_t26Q zZs!R-EL_^lEv@T~o-7O3RxleH-0cPD>d!PDU?=8Jp96aeZM?ToPWW*X)VVSBBhxLl zl_8_Au>q)lTq*2zR>FmR!nrt1@xckxDe&zjpj3*C??oTzT-uq7s+#KzOvZ+LP{wPh zY<%Ub7;ZU{%r-`K?jCw|8h;J^m%H`q>~>l_h3;bR0(Gri2p@?TJE4W9s&BcLu2!{b zkVj^wg5rBexb3*o@8eY-{{@Wl%|1=CpUwshd~Z`lby9wKe%)FYN+26F{$oHX`$j*| zeS8`BcyzA_9OyHUQpJeL?wEvceViSG#Y1$QchX_nhUPf$rJf#A9Ex;ZC|dS%Ra4n< zQ0N$%tSv?31f{fk%hICDvW*(6M=F^qVUKm7E#@~A9Wm9us0YIJqAKQ^DC|a11s1Pd zenCG$jQ7cXeb6eNQw>d!J%*foot!)R4D3Qhn2;@_e1~nM04mG*AAjGbmYe^fQ7;*l zoJy->EtPGXJWi(711M;#nAVRmWkDeH^^P_$SyuC4rATinkb!SB;~6#%gW%k|(r!su z;_HxDSTx$o>-KtGWRSA?H~cVb!m-&G;qE|3&)ufHh4F!i<7_wP*plNw`JG!@?6Nvn z^imx-$_~*YJbmCFa^^UL_)znL47HSH_Lx=&*$p}DQ0{*wdipQH?)kFZ$pQhO%}h8> z7e{1X(x<@L8Nq6OG;vUjoo@@`O^-^4%~pra?3Z$fF2_r}VqdkUXKhOFpv+MvLyU|d z6&Z?ue%t5LRnRv{vzcz9!;}TO^u}Lz=O4wx;B9c&`Gy&2eWaY)Hx5~C4*|Z1m#8Op*5JjbhDe#3>Z7p(u zC#x*q;93jHCXq?l3Fra&mkdb)A|uoWx%c_yt;`jSw^8}>p0vHndreE(>y?|qzB<)q z<=TC~&q(_esF|9Jk-`)YXUdCxi@`(suim2C_6!g834%N=5Q!@mu+ijQX4ia5V}?pO z9$G|0H^hh(@2R9w;!O=sf^LuFV*Xo02(gr6BA$>{z;9n?97NW4*?+4_QsUN90Ph2; z0_F7$08=Cb3Ztkvs~`{3SPcE-q8O{vcppqEK>m;=7Wb03c1GNKMFWFFrnSMIj-kD zXK}dB3Am8HEYp%r-)3a>M>u;zm9hrsewU-D$W8!aQ8R?I*~v!r)T%zgb>!F+MBSV~ ziY^dd3d-FJ_}%#udocOyHIK6i4?~*Y-3%C(iaL@h@lO5;-%%x1tNo{`UI&!Y<-up2 zIeqogyBU1~Fg-BADLhLOfrZO}L9P&~x{4dD0y~-~Mi+RQ`Ffh77!go&`_O90r;`!* zRDtf4HX()(=OaNq_uriu~NpOZFI{D>dGI1{-;5LA#5-1C5WrNPLj2TLMNmd;@CAzlB#mZ!_oUM}? zbZ##={at0jN5u`(#7W2>Sp1t)Hw%~BT;6G_j8m|gjgJD(A&q)wEhm1b;$#16khqr1 z$}Y~a7v1W)LvPG2te-STQmT#DXw}thD%@M^hmHPpgDi zHEaFzoi~LCF4fEbnz8P{rGLE2sFlj;vtQ*~rYv;U%~E6~**s|~_&eejCt-s!H5^1s z?w?1H%;p^*1me_WCO{komy-*m8o&Z24C(M>vu;cl|!>!P+4Z1;gSxP)R0=B=ym>426usAnV zWI4zMz4Tv3Sq&5AY>8&fOH{;CT z%mkmVw9^2yiktDBn3I@{{Q3qnQ-y{dMuY(14|VQTF@{k zAEYR}Hl%8{c?98ytk)|UCkhN0gg(@dK&w6!7AXxCDQ0Zg-&THU=5#`jG~kv#`lU6k zcHsSl^XL!TF0E(U`j%5|RlAVi#o&_M(~TW>bi6TU${Ev6dTY2z03=B}%tAX*4`v0L zfmFwLo>~+Dx?*-?Jy8F$E17wr1DUBe@lZ@C%eLEx7FwhCw+LACF8h4BQ^d|2j{1mB z9>rWyyH?3_xbCkdbUD8x*Yxk^%T%brPs9B}8ad!`D?8M)2A~iRAx?gc1_6>Tuwt@Q zX8{xUeoC6m4w}=;>Us|r%sUHb-?5e)S-p;`=2k^)_blJv^#8@KO8O+9A#BxBu_=#F}>YYbxIPOq|2Pn6F3AdX%S=aw~*dWz!$y zWDqRs9q0iYY0Fh(g}&4kZe(-;F`XQ^g)y>FH{94~za2Fm5EQRfl?goaV5QyG*okls zA=*11CB%emV%VZfrA77YR(3zF<-|e*-=itn9NpIHtN8*G2{K7ylC8b#PF;kJlD#JZ zSgarMaw{?r))B#*zrmWB*idCAQ{qXQn~?kAFz$Tjf&`Z{cLd=YJbk?+{IFt+QcqRrVx>$JI$>G4B6cY>;`DTociXVQ zY-CWVO>>K7AOjt$IJWT@CIw|~YlP)Tq80v?{9W}E*vVXXu#i0IMBcQW2-A5C&+1ue za{TQ+t=~WhPb$|PX&|C&K)eEkJJHH^D@5R?Xh#L8pE~>|c_IKc+I76-) zNiLc7;`%!U5MK7F4mp$pE?DEC-^JOK6`ygu+mo$_p3jxrrmTY^)rlDc6a)GFmt2*c zff=9L)CI}1DF9Byvqa;Fs7`u&{MnhD7Mew9ygw_YXlp#q`ecgLVSz}E>g;h%bw0tZ>t9mFvIvf@AF z)$5JaIPkopHTGH)`xo-+AtOb&`ssAa-8uZX+KSpy!z#0_K26Yj!@E8&iIH8PN6@;! z`t@O@9iskZjW+3am`WjjP-6lGg5nIrTFo1+h~Ji5fCiMo-M(a8BRT2b#`4uPymfrTKHW)Ggh>VP{@?+>mOq74Qx%Xpk4SPBh-k*Bmp?i$z+$6L}Y(a_g-l58H15 z9*RwBe=S3+*x;;0^+Ou#!q)hvEh2|cB}1u&R(7kl7;-bHbR(ORrg-JksQ`5Sb{0xo z1nlx%9x@TMtzWZ_Gwn|H>p_jNh52*ThWbqiR8ccXfF7M;Fa z`&v~=Jt2^#vf?1Ot%=fs&v)X(G66$ z0BpcrWl8H>jK}}?Pn4s_n#cZUSaI$q91jkgWDYl`T`l^!Hh>95=8%6U7-He94ecv1 z?VC%#_j+yVv*qM6A>k#@%k}h}qMGMFojjIqFTYpH?It+TrrS=4(PL2sXNfhIyEZ&4 z%-es0SC4&H^j5UJxHo=YiEoKq=iYE1%$u$@nNB_ZR}_}ms3lWn#c)u8cna8!e!CbV z)?b@WV(Kr}y|%N+A{gfY-I4oA%u=T#cK0+Io{HHWeY!Kf>>KA)%Iz#d9@F3@&t8(I zJ*{XH!6&!Y-llTLTjn_JXor)Z*wR7PaTed}oqnX|Ptvb31c`-k)_irnVsD$VoG?m8 zdb|wEpqKeYfU+x{KJ_=pi5WL}l1$Vf4_1&R zw`5TR9Z1YzTQt3SXGGbn;FI6$^nE~6Z1gZbWCLzQa6h;kl!`QGIR~zwI39-^G*fEpv-RG!r=U9^ z{wVtT_uboQJ^KHiE1ruwRW|(Hu#7jI9HaQt^9SwMWd4Pn!>Lr`#(%h*Bs6)f_y$Y* zw*?;;ZH8^r=dIvGB4MZB>(lYt#e)%1$T8Zkmtl!?v`9Xb8ospumpaSp!pPgMEj@=O!a5&^$lm_?5DTnIVw8Lhh`w8AL2s<%aA#CO(@T>sUFm)rgEH%3<+Xf1wL z3SY;Pf5-Nx_R2Y8X&qm(l#%*3Lu^E>5@`9{q4#L743(M%~ zOhm=Jd3+k%GVAMGCKfY&eT^?~cS{M9>446n#*xW#4A{ghx))?vOBu9VR84iSDRx&* zdM_flqvbM0{Xj||@3Giwtd}``dktwiLez=|;IqWJ>nHOrXzc}!HljjBRu9O*DSO)u zT6)o;Wx+AEc&TXd_g&q=x9RWI_-j6b6!MnnzwG}HU*{N`+Y_$+w!7`Wwr$(Cy=&Xt zwQaXgZ9cVao4dAcTW6n-Z%*NuPjC6E+gNDvw~atL5E!-As2`j!DaSRP{VzJ**O{4$wAkqr z4Z2Ff#Cxp;8ekZ=9K=k@)>EDtB!vEgsvHT330?GOKa%{fvisQisaAg}kCw)s-N2`A z*6l{R@nia_GwsN+is#2LR>I}^$))uci&~;VTGM0igEXWb92CULD0}6U!$=%2(i#mw zf-DUK+II{dqW+61$)XSHg2@R@L9`gMd`PO}?^mDgh2Yx5;KPTR_e*ZqQ`>i2_vMlKwbR)Qcmjg|Mnih}<6 zqk@Xa;c)=9rcXU`b5+7tRHKJz9gm@~6od`qQS18A@WB;^VqRi|f^ZCreI~nEI=LCNszu3I-n)FV_<#|46g?xaglLEcG-7d?{0KK{`n=`(1u5u(b3;cWT!~WcS%U;(b3~b&=iYp zf+U+jU)OtP2R$GicI=Jw$x+rMi*`IFCi>-u&AGsk zYCsh?On!Ew&Hq|0;8rudtxGNjXal+`C!%>RFh(cW%r-&ok1^f151Y4-FWU13QO_hS z?pJ7Pw)Ijpz8@@!c=cA%9qV+I&Lim{8H~#l3JmWi0#bC@kiBCHSFFu^buNqE^U@Yg zhOdwZxQE{5;O8)51x_2Gg_^1Ft@JxHr17*@bv~ zN)Y^>xL=WX-_T`5S0^ZdDNYPI;Ha{K?t6C&A?!RM1A&4#^tbzKhwabWB5KLk0_m>M zH+3_vx%akOW$auOT21w zHx-(%$|$=Gp;9iRuWyWR;Kcm1q!dQq0pN2q9rJ7Gc#983o1_Twd!1)RM}&n(qlTYr zglgo~2$$|bU;VuO8#)#%0Vd1t*B^yS6J1ESzRNr?z$Mfjl$JKmGn!JE%keXvFdUDTU3d}hgXhYUh~iWns4lTKPiFw=G*5} zQ~>g^1Skfbv}QfvEB)Ksh8q4y+h|2#^T;I5Xj>>UB^$pxBDnF!+t&bDKG zrdh}eoC0Owd>qb+PIke0=&FcNAKLc6$Ru(bP^EiskIbHHnOZ}~t)@}32Y)?P-*1~h zvY&Rl%B_cY={(U4vxctpFm_Vm$ZQ<_;5B6aZ$s=oodFQo13&$t4f!=lvF`+1iPFV_ z7_(e7Jx8R8C5{YZz}9%J_n&`V_8~EM=DnaUCdoBGxd%U#MpRXb#8JAH%UD~ z50_|$4CM*4^1@ngZE@Ai-J*$dfaKVzqXM0HlNkUh2)&blpXG(uwE#KJZZHk5V}W3u zNR}Jby*L}6Qxe0ce~C9}67(3@;qttD?RE$i8!uzM(l1dH!lFivfhlb<2s67wp87_9 zg^tDgZYrl=q>~0=YUWYrVfjWNvYjId2{qHyg6(TVrM>@GM)sdtEEe%ZX*N~k1}wtY zWB0hNAon2!FL~!rx>=&~9PmUeKX7GO`z^9hm#^w==TkWDj5jy$xs1#UHy-@l=4l6T^4yaB$fP@A{ZFiD^V;)SnAVflUqUSO7o%B||0Mxg#tXBT78{0*TIiZa2*R#*YIiu z-IYTnskNkZDtAt2q7pzZ)j}tnhv9J5zF%yY@(GLdDdm=j@|z6Hu0SgsqWCF@?V3dD z_bHy0S^V(pA4PoM?m1_29j%AkE90zgu9Tt*(+wm^9Vq2}0P&|0gjhR#`n^$%fb2X; zmou>qRw|l%UP!s1zsH4`f?t0Aylu_*@sdB{@y7X#JGs8NGWkPTi)4dXT|ewRi{jjg z;R2ogs zw1Pty8zLoRpAXM*O>RFsSNi`%4hVvhLyS&8fz^Ue_Va2BgCA*j<{L-$o#VE&0yfkd zuS9UmZ}NpyiF{&x8jkJ$I7r?nr&{#5@H)X?qU4q@(TmLp{mAeV6U6+J z$!D*2v~aFPuU^ZMg{#f7&S*N0<@zbMxN?s={~cBZni*5d*f5zy_|JWfV45OhBZHCK zCwh#4DQ6Zkh(D;6$Zo(|%g)Z0-D(AHb%j>9P3Fs_A_>3^6ebdd)zpsmzIcLCm;j^$ zxnEAm7fOa0UX+PlVNzq!s3EO4ogFJP$ZKxoYaeI6(C_{UY?{BScf*kBWeHyI$R{#X zZ0ELwzkY1;De{ExuL8z4?6{``B&#`c6(Xfto| zWA4Be`CM&{@;UBMU2tYSC~kN9SJo8fHzcT$Z$Pq2iZJcfGr2%!9xWYGmmp7G)Avs> z8R^ZaoT`xFrV8U6LEm?4hFi%3f_-@=u(_-|ZzvZK*`@m0uW1B8V&rIE$t1i8!~B@BPp zVNnvX4H!Q%qY&s8*zUn3-)eFL_^axM$ep)rt5+s`N4pzi(Z1j-OrdxWEyk2%FiJ5joaQ~B)5eFf-m^9R{CuYZ1V2PRntW13*dUEWr2#pWF|rb&>*s`;>X<3Ey)ydlZk<(W(iIJqA)#acxW*~4c;tzzR-9w!aM?YY!5 zHq&u6t_l(*Q!~ey4b?kK9WgBw?yuA`(;FidZUA!4gua7RoS?U!*G-LD%4MVBvPXrV z6>D2vd;KL+lN3X$qF5ef1~98GQmMhg5M`U)_-II@!8?xUk1p2>u<)i$rNT6i#E&aT zkKiX7w0DDg1eNzPAiQukI20c07`yIsYBnaw7)#Z|_?z_Vj91flL9IgO8M?)7Aj0zQ zjqVWW$A(XwZMv2HH}4&w@rXmjLZzhDkRL?0a2L+#e}vphxJ^G+)$XQa_bCWD8E4z7 zjAg=|O00IL8D|FG(X%^jbbcRZxF+xWE31ykuoy2Lm7kt(>s$`8t4Fm+=9#(9gro^g zkurCl3$H$4EhwC5gar|nBwCO*qsRdQrx4JcuAy|mSS0v4{ZP$>wyaHfWN`LXW?Er- zfh6rZG3=${oX<#|`ZX?+?sPE!A#&Wfyjwf~p}Ar*haRFPE}dHJt*+5&hK;VB*>N#b z0q_Nv&;86H!QdwAgn_jmz=MY1(IYX=I8qMu)1No>L*-B`*vv#v->ok!glHC5tQy1` z{~XaUL(5|=`}gf&=r|jtc*M5_PaTcYWp1eZFM;+5>DTDzo)yFX@YhACm0YiNA zE(*2(CKA+1EO=EIqTt7d(R;+VEV&I{Y@bhX@UT>dlEzMsq~r@;S$D03uRvfNBel_5 z$fE*g2xPlI3!vFUAxA{!h_X7ov4d}Ak#CK%AFow3%M4ng_c4;-?V79jR&)Fa6&l5r z`&=BLC^{C$aA1YiWk8d0Gz=-V@`y!^^ErF1SE1y42E*oa=R)oO;!prdjW?y+`89XO za5#aYI zLr5Tkwh@{1%dz7+Kc$55ub^D9NZMMxc;KvYAn9v>TQi-$Zb5N_scfEYv~s2Wa$PXo zrDJCi$+Z|N#mUK(i$0&uq6m?D->c6e^A+wU6SBI4gys+<`)Z22$sS(d&gcQQk9Yh# zj!w3KgX15E&c8`5t!1iBtro}5s=u2>cvh)S@n`kdxodZPV0PBKg4zat00;g4i@HFs zAt59M8CYWtf;=9Hm9jR#4{XIF!o?4ZpbKYqZ4aAu`e{6SfuwG-U;EwFl+ytLeMj0;;3MQsJ-_i z2^6EMRn4_4U&$#i?{dIb`CsXD4NaHrkfi|40e%Xs~*wS|I60z z7&s@e2S5>(miT3mlQKkORiL-TQ@VcR%mjnFRQ#=;nQO&!J5f}E*kK&w0gWB4OHLHO@hyydLRm)k_ z@*FK*oVy+fS_HoA6~SCfuP<|X4wimx%!F)wWEy-!bB{|j^E4x_5=nVJ8SW?&M+IsM z82qUuI;-us?Kx#8nRSnP6Cni#P`mQz(V*QAkNBfk-ZLq$G(dcAZRAy`>t=wx9W%de zK9_FWKKzmyO~lq*P%4qhgw>)aT3Uq+BMtnga4^y*rfq=f(Phy3G}=u@Zh_kdwrc;H zAmKC7XkHWy`83LeN(#e`zTXHBMS+p>Mesq@Pu;f8DFRp89;!kZQCi*bU_@CTbyzqT zUTCIG2PI0Iu)#tb|Hx}@ma2m-ZoV8gE;ne={J4PwzRtk6>e(zUS$Q;&Q}LBVV0vP7 z$Rcfurkf~eU(c3pz3NkcX0(y4BczWeOYt0CI?U;nEuY4Q2Gp2qqrT@3y^$1vCR$q|x2L3E-Zbgugz*2KfN0l?a7GOUnxW^K^=l+yGh1~?^q zH_OsL>yY}V<_@uG>T;qE1jr;k!u=pXMGP{iJ#tCo>LMC&J{5XUBQNCR%zL$uOZ|iW zc$HW{HqYEjILLH~Emc|C5b|XP+h%#k>W2Wt>a|dVrvxzP#HEQAh>^nqS<)-xLy2@s zb45t?U`~HiWK3}NbCoW#d8i-OX)dhEpAKU3;_UB422J};(dT@1o0>5xAP-04o$T||R+qCQ9Qa-8Y!n6M zg0pBCuG^e13uWf|@C95+kKLO;Nbyr!7>VB55@&2)ThKLkJUSmi2+d9pMHARs$vi!f z@gBNFPm4lsrd>GNlXTa0GZ_zzW^{-MFm4^_8uUMntgj16jMNF`00UF0P@xdhJH(u6slefij#u z>T0#qd1sXRFPDn(ZT|kUCYi?U@MiwN#WFg9px4>+%!KwV_7S(7?aG#01e^)Dn4KR zW@*s!SuJfDH%f^jx+b(;0=ej-jF&*cFDFI0K#%2~;a;|OCabFC<#Do|Nu5f)VyiUn zXUsBd)XOyGIsrvzd^(*+t||^3jU>`%y@R5S=(JoG@LsP7amQwh7(>TFrL&Rk9*i_P zD^*MKsx2bvfyIFwpn!28DMH2dY!F(An{pY8JLeVVICxn$oE`-9`}&OgrOY=)kfH`p zW~#Ey;K@8y=wM>YIVcG!f@Y-gWahKbfJP=?1rH&b=0H!Ep+T$RiiwJZr1z>Xpdja~ z-Bt8u?K&5-6?;b53W>@FS_w;($h-94id)+)<5__QTwg<1%>EHE1>P-*aYz#wfXHyF zXVc}&n+7|_qU*4MoVFx*6}d`zdsiH}3;cC>`WAU&u{Mu`hw+5C!#|$rev}@IgRcW| zkCaBBZJro=_Zjhv;wwz4j(_dx0!#YH`F5HiyuSaUjm-69^rPTNkoE?m+i)>UO`=bX z#R<@&s7vY`5@=Q4pLgMXyjFVb&x%L(RUmGYl*e2-kO$kt(W;>$q(Fj6NJ3z*agzwu z9gH|oKb7TzH@w=hIq_zI)V%9b{&z$;CJ^R*sLS4S1X>p}1<9_sC7yP)RVr`nC%jES z7H&@@ONr(r)WJM1Aj?V#KDDfV2DGa3GzUs3A7?k~@C%QuVH@)($RF))>j& zND%+tbt*z9D+hTEnnSJk+`jxi-N}nfWU!e-^<5?tj_rD4@L9%(VE2(<3N?eRMU90;s)p%ldt7zhK|tVx z%XMsXC#kE^>gV$= ze?Jy(LF8~gV2jQ*Dks-%I4#7&_QeO(x)U?L1 zR449EL`33TV`0hK$#xyD_csVz!=#ou%0IW_PL`aWSoCGa=@X-{ViTc+`5!W}Lp8Bz z=_LrnrQ*SYfm3QlXep-qBRP`!KLOylAc0*^b7!?{P-aih=1d{)uO*k}o^;MM3$}7N z3`W(zLZ30dQ|m^K=}-@bRd`awdlA3iiw@R9LB@0JmuiEw9xS zxBYKDlN#t6n^M#D?MaH%ye7Jyt#~i+8D*fEWRFxbQxo>MWJcN_3NZT)ju3a6*VI0` z&JbO)kIG*h44lPA3cf8&%^8JLfP(rD{4AMz^vG_^*v?DgO=3U8@Y30z+h45!(CJh8 zY9WAS#w-E$sp=v&qe72X}v!i6)Q_NjSC*fmNHR=;v#8fHyC3tXN&-R6YccK-VMi7Z!KXA@1gp-qXX38lkt zq6}RXhfJZ@c1O#c;WI~ED4aQI?Rdl|eL=F*%lDE_po|`IIoDQYj*!7(I zM?IIX(5kO$kV)kH|HdqLK&+q-k3zaa;VX|1E&se4dNw&8+$fc+TBme)9{5Ao-GWQl z`hi+5JqS4QF;eng8MwJjj`5^OHfkd_y1ijG6n&tR6FKZ8>-BfJWB)eS1uo$6EQ5Un zfjZUtUV~wP*RPB}!IK|KVg+d%RnixfFaZC0gzO>tOr|q%82a*=H94fOd*>be$F>g_ zIt%_<9b)*UVJh-cTO=w-TvRO_-H6e?6@B{_>lgdm@i=7IiBxdlg+RJM3Kd$c&{{|9f!9c=8#_Y2n8$O2WEYzA}EP z#VrvjvRvqq8mQEgo0iwQ@AT#=@q7*Y>mWk;5Fh=~RMF#2e>1Tj3rRgx^ILfGmxq@c zyFT=0>1-e~HdB{^C>6$NJ^SYQafmP<5K&$ncW*^&(C29eVkVO#@Eg z+_vU90%IFX_ydnff?uWTRK^c9)*7wI;OR1Etx-C)B+kh_8F>T~XzRS}G>@h{K)sk6s;k%7+fMEQeIJd> zYmCzSU14@7ktg^!lH*luhdf&d8)|(9;tN0)ngB6$$AmOsLKP`iOodbwt5Xki8e+a9 z5?37nigYJJyiVP5jFHX9xZTmDcH2(_AZwgj`vdNFCyAD-B5KUgKMq*c5Ot1$)p2Gg zkxo?lJA;+|)-C7;2?^S*EaQ|!8D*-Oi+x~6vJFRr z`H5^k;0!qH{_pIW;wn~szS+WC>jX94&C;ATjIsQo>+UdTU}8D=0{80pZ2+QE;<)=x z&LFiBq*C+49BlSNfYG(?<$Ar8^h7hWtBX-l#~WbePVZe5_+R+4PQ+Hn+aaG$TY#m# zz^a1&n(xVRxybRk{M9tL%J+ZU;1|EO2;vHD0v++?vw=@J|w7w^UJ^<~C%}(y5DKZ1LzyE273Jw${eWQhz^pSVJqcjs1 z>8QL27aTl=RvHnfx#tpYrV1+z3c-mxgCR@}uLf)Sv8iw(xgofS;85uX#$>-$Xc2J~ zXLgCd+m7CiG_0!zw9NlHB?m!5_6G19W(3I51_gN_!g;AX^?c0@j<8g=A_QghAu1yk zPF&l+1Ta9-j=lBmmP|}u{4lOf6T@dDzuMeOusWt%G6=F5OvuF@{K+mkN%8gJ*@Ip(boiLsM$^Ajhn&varlgElVp6Zo&Ad<#c*8P68g zoT{n+kkh#!>7kInF-G+MsvJP>9kdLd?_cQs@02zG-lo5sgk6s%TcCC9`>|`Cyc4fk zx8bAu-gT?$Ht%vE#+7buN&U_KMsXLV{r+q3M+&_#NUe-My08q5p^T_7zdE9tUb~uI zVo{>5+0g(A4LybE_hvxE)G+P`!+4eZ4xn?|fnxk|Du+HJL(YQdOwQiVo2U0*yeT(B z?s+X2IH8`!agk%e^82J~fm$Kwf&T=mb{4jA%n{S)vf+Es|1E_1$IWv@SSybWMEI`* zF1Rq#|G$Nxoc5DL3V(WC_;`$CgB935`i2X|;MC|N1c=l^Ff*mVl!N|o=z2vW_Fp=M z2}S5eckd`ofkc@~06@kfPK^u0_h=)HXL^|zKV*IC_EX@{gi4|82XBl-h2a!r4Qg2s zy25b^1^;lGg%pa}9Z~_$BQuK>RC#Y^8qc9`9h7LT)ERG#?+6n_#B@FEQnn#@*sWDl zO^p2#$)onnR%8b0vLggPV}Ymrwa-Hv?tHXRSv?l0;6w?R;-Wp={j!;8R9sai8Q?%^ z?I5c{_m1nYtFnL*iob(*9zlJHmoyD2id`#AT$V)pEge!Ldj(fq z+P04?`ZyIw2t&$R!p@~hw;TWr55_jCj6~>m@ujTnUBR0pdRvrEMuGO0UFZhQqU9+3WkDX^_r}f1 zeM>%#!M2%T%y~jUE?c{i&S1={n4oL`F;1D0rzgK-I%-&Pu*<#|Lj!jJdRlihj4A!4 z9<7=$Zx8hZ_x+>B)h>t^PBNtqG$Dzxpr@xcH=>_bX`H5o0UOtR<|&m$LHc9dpYXLs z>Tj9J_elL?u#L*%8C|uGW1D|0qgOK-X4Ipsxre5ZX!Xz$d#;;S)UTeOtLFlpDgK#L z#yNEnEi(*-$}$sD(q&k&n8AZKh16jItXag~wLvY0EzLpC#6LVwnQV`aEr*c)H0~^M zTu-sloZ^tYf5g%#to;#9Dl8;7%2HH!i@wdkq`&!3qZhH&Z ziKg?k7A~P5%0cH8i%x-3qMGV_bcP4(UJ=_~#2br3Cg=~Umio2xRnv9$CTc5%&f|aS zIFd&bJG+(JmUeIDk*EnQ+x(HLTRH{M8^$PHf}!Qp`D?EyQHjIb3@^^{)Xx%~2K_l_V?evQIo z6liamKDgXx{v~(fZJkM9=QyvX{vc_I`5{9 za%2(Dj(PsA?B;GAh%o5V{*vKVsod$bKp;iV*_ukti1bD=ro4n@rT7;Gue-vxhlCR? zyCXX_H`mXvmMpy+E4CwK3k6Q7l*LpkDQ60?Mz<`FjkOo2Sjqr5eKXq`_MFLpaLG~y z{@8LvCrSqX?LDH?uaDD;N;3^LV7Gc!z12$G3z2&3eAy-D>A9ta!#2LnGzV&#k8V6UbH6FrZL%V$}l5BU_cf0tI|+Y4O^BzRS@kURhq3 zRqE*I-P#w+eH&H9(`&}>XcdVk>R@K%IK^4d|$)!rwxoO zmDuHW@p4B^n%d)Cxh44hM;Pdhu!=Z(Q)BiesX-;MY7cuQz*BR)00pzLrV$$mBj=@N z@x05)*s>(`hSKlCM4EEIzm=|5*n`UTdl@)z=B{V|9_||yXLY(k~ zl(fqS`Vm`i+PW zishY7Wg}09N4YvLW(GI8Rc+n9T0*Ynydw?CQw(|f8FA_wlHI>Hh9!QTEh=rhVl}AX zu*?{`&uY|axW$*{CjXr5A%8znMs-}IjxN6`Ry1r-xDog_Sa@#E5}sYmlwDpNM&Cc2 zyeaBiApLl{c9C0I%DpXJC_cx`xM2FG?CfB%pS`Ds15{UPD~}&6D7+fq3}WFpkyd(f z&IrhADkVnJ+-CKeP#ZYXD6ORmYM&?`j+03>6^-OGi0y z9R+xom071(C(8jUP+>98r<^@^iwiri>xy|^=j9GQ2c?SqUyuJ@Ugh~6Y&>4I_TDWx z@O?chS}A@dUz=K|_m`gHK69YufYTBBXnAmTxN|L3*5&%-qf|Iei0i}FIW@*WH%kUnP7mwT#`LcY!yi zlh;e5ZtS%jAw#y{wc_Baj0__Q%j*P{`D-9g(BG5gyzy0t7sw z!yHU18i;}{DOU0FC2JZ7lD;c8rEYqjD#yQB6=+tI|zAV zZAnNl7)1K@u-wE;w6NyjrM9@mb(8y$RmJ`w5dlEJ7Y(H_3Yv_XL833Va8FPI!q>OZMyBxCZys^ML5oE_ysW&Q3FOs4v;r7;)y^;*BrBpf`49W2Vg3} zB(3KH;5T+)kw}8JFI0m@Jqb{~=Sfy}Qg~p{bUjy9{^GE;XX?P{CSHZjKQ<}b;AO2? zb$MJ5hy-!7Z8-J}W*FHg>eJ-mn-EUhGkb(*dm6*Z&$M*bg3r0-Ofp&PW3WB5O5*(b z-PlNMpoCe9S4mS_woVM?o3$|5KP{>d@5Wavz-R}JjL6%`fQ7mbVq`5>SyM}UT(J8X zn5XV_sCfN8kmn=7VLBf2o9J={g(_=hNMy>_x?q9$>IT(ik^Trsu1$iTY~f0y{d{U_ z+*!c=4u48j(XzNCqNw6p+ZMutl77ZtD??4);<7K#j_;IV0$ zNSj$352IPRtwO3XvXw0RiFDMw zNBGd^GMh$pYCi1iq$)MinKxnCjoireR=LVVF6^JCM4-ccF^ z@sbz%Sxp`Ri4s~iHIwO>Plpker6}`P@lpv)VvQ@kd(T9-HN2MOC$=x-sG-|H=(&2{ zSmHv-NeLE?WYlROdQ7g2Wm1U~gR7cGsZ9k)Dl;%c3Bw{hr8(Vijejeo0Av_U!Wwe7 zhnB?~D}qkB!SO1pppOD34(Hew<2}nMthTJOa0TbS^);)>3s^o?E{%BR_@|^VVHg6& zH1t6?#LfxY>rhnHNN_-&iaq8ns3YN3*ulkzc2%31*HEi>b?;?77BYU;;$m(qZ#YD* z$Ciqo=DdDJb(Z2x^m$WS7B4Lv1b0W&?i9L1NVs+}YSrg$3I75^cA(1JvYO#M@#DDC zRCyPdq|$mtB#kY{CKc45sM4dG`cyP@u7wD04I37_%&zPY=kX0)3T{lHq65ykE!G4> zG>i^OuFb)v#LK=^rG|=9(+0r47XX3-9vAkOVHpUX53+*DE(Ckqf|O z>BHAXsKRkvRgs=>CW*%#sV$OtDx-&Ec_*eq_Q_*yUni`Yw*l`{GzVtm=t!-#%zdV+ zXf63t{Oh7X>xImRlGlbUhEa|Pby=EB%hv6ly()$Q}>;6ffH?7G;C&>7dw+K zFK@H_QtPCF>GxQ20R$G!Y_9DFG<7r9@s8EfII0e1*5N~S%7f#0n`@~Zi?CX-QK#cX zf^-^Oon8!7#xgp6updUBrD11p86~f0nteSAS;>+*2Lx#RZ`+q&a*G0m9{Skd=Bsct>1UokLYTB^PBzOZoUeSV(Z_PXlJ68wit zDq8+Nz;#$y*!GLn`mkH)b(mw#pwN?K{5;-p*jY=qViv#2E>G@HU_MFOYV!^LeqgzJ zuEu4YLv%r0kmfu^HvfE{WW-O#OC?YxWV7G#ID4+%_-~7m`NdtE6M^>(!3!C(5rqFi zAt|@DcjX(&*S+*;7~W<4Dw5BzC*!;@t&Trkl@jQEt&l{P#u% z4E87E_mpEU9Y}j(|@!bDxEzJWdCHx%M zgnEDVA0Z4Pg8D!0^iX*1bhNE)+O3VpXb|*~H!^nQZXd*v4|kWd2=Wdh5vNWOx#nCOCAr>~dplqe{X<pwY+?!a=jF^1Jaa;rr6jl=>@#is!oDthC`-$_KPo9o~{&SA9Qsz&o(hMhWE`JyO zwF%S@t#9^$?Zt;E#T2vbeN_B;jraFFCH%S1`(_sPewmi)anf3-oNpMpTKJcx;J85~ zU-!_gU%Iv&>thCK5c`$L^j`!?qHS>-5H+LqeUPsXQ5I{i;MXz+-XIw*shBy{UWGc} z?u)xIo+>Acq9MDb@+40p+V~XMCU|eJ=02IXFF4>35gr`rxy7E0y3&j+U*57lu4=_R zPzAQ3+V>w_X4vZ$*?&q4>xw?;A=rk`zC}Uit~ zwyc1nfTy>+#Ym?6#Foo|aG$mlf3B>A?9<8sx_z;gb7P0MQoT~ogzisfzalMv)rGEL za{8Ob3tn5{*q;&w`H)N>ni$dKa@pvr*g4|wd<}{!ygm@3sp^xGs7ADuG+2}%*m6u@ za1nlk5vy=~T@@LUDl^^*NO?O89Q1&if$ zB^5M%Ck+e_edXfjp%gKD%>aM-f-`Ke_{q6WwFX<50ysn|*5{^~60P9STE`0=91^Az z7C8R)k{o!w_2qZ9p{NV=3p-#-r}>e}UJIQq)9xKx^njyY7=~Q9OV#l3K<*RrSebIa zMM9Um`ki^Oon!p@v&kqOcSsprB=5<|7vQi>jC=S2(TKbQd1myH05(`yO@1?fxV~)Y zi7q&vr439aq}?xG4xRRPmj<-1&pDHle8k!-9@?73bBTcxr% G&X@Yj>Dejz}S6 z0nQU`qiLxpAB~+d9CpYW_VYiNoy?x}Ult@y#X(n_22Jiwqy{gEAmxKs*@9D^1Qu+D z#-k#Btx#Sf;daPZHCV)BO@Nb^cZr#y)s6eVHW%bfyqft-%g9?71;4tn+}r@8HMNGj zFVbsOyK{KvznFUSaBOyB;oMZ|`60%Kej+k58>}!z%Q2f>NQ8`$43_^b==nhe&UAFP z@`0JhbEi4lP&VsNfxa#kl*8KhKfY(cW5a3PM~&AGh?@gfm_GB<){{>MjCGRQ>6dWL zkCE0fB)wqmsXIKFn>bQc7?G{MYHCOza6v8!&kJQ`viznD(@fXLrTJ;r$@t}8-GWyB zjwI2E{iiVwtkkk)Wba8aVD9`TvLL@1$ae@%<}fpjhOYPH>maVEPW*3WpyY^JYfbB4 z7qBay#BzR_^K+c$YokXWq6lJ9vqC(&9*(H%!h^I~HwZ8~(ibTmk1f^$JQMTH*j-zo zD?zDfy-Az%t*WgOX@p52HKJj52!Q4v!96~)q`z$Plvb8HQI6R6VD(aL-t?Xm*{_t( zb&Zu|FtnFZEZiE%B&^ZJn{z~~-*-)lOl9Z;W=?srYfCG=mRBzw#c@BT;ltuOXjQyr zHKxf~POJ;F4QGy<4D5C9c_v_+K+6qV4BM)Mr+%^>^k1G5Kw%0&;*TeB(_I36mWl}}MW$JIlabev6D zO^Q?&5nzQZ*i5zX{*^hBLB!q3ArRePuydas1Xfn1YE8F)HO|a!9XYjDxkj?wC&|Ef z!?I}k3e-=A$SoQFVN33SP!XqlHh1YyjVYA+9Y`nH$UFeX<XmO=CwV-fFXT0szd@9xp+AHz0L2(XvYBA)E||OE0QCVb zuv3Z}TY#aZk59g|$c~vA#q)!hJc`f?6|ct7UGr{12}KaYP+fpMk#t5jTz(_YKLO1B ze(ouK!EcW9Lde|ld&k|8as8ZBym=OU5*>W$@l^8n5jeP(33q=aZ=rCT$+CE_qw>f; zQPlT53dPj-IO_X-Q5J5+@-_RVr1yU5EO{`?&s+Vyhv?^`ipl%ac5u;cGZU}R|3n;2 ztb%-6(ASAb@hzp_@$_}$nh9GEugw*z&`Rq@`u64mG?t_k{ZnzaVKD1Y_~yk^fvQ=? zhjr+nC>9&YB@_*-jpX}x^y5EqrZSOwoJMiAq|nS@;YRY%WNq8@;gd!(1vSO{|4Nia z>A>#h7CcpXUtm?_MMU_+*xMX*V(#kbJpo{Mj~l1 zhy|zaqSL3TMae>JIpuoTIyW{_G18B(JoI_uD<=K{+BT{Upq?0<{dX07U?(p$$79`W z3b&LZ4_9X$;NMPV;-XfpN61iVgG6lmXQH3gr1v$Wv*-0Koz=w3w#sU-ZlOp4nv}!2 zP(uP6251@;+w2_3mi+=%L;;cX?-oCoiK0k}EP1!6CQPe#{U?;X1>Z;(RYtiS&NJ6s z)rno;4l;QUM@7E0eP)VlwjzCE$i)Jw)0XNYbFIA7I0|a1nUxi!MfA3M17*tfT01;~ zmSnTJL{P!M`AGcn`QhJ|7Ixuo>g)llmQXB(QK~fHc_e536yig)wMD6wF|hzwmTqj? zM(b5R?EP%VcZE=NCD$woeQm+kuB%l7>2M< zT3cgAxtyU+w}XBjkLfZSG$-m*&8ba;>O7ByCr#YUUbfrPwT_4nUV!&&TM2SR*t2;F zi%~kQ*yPvAsJV~OBACy9sr30~%b>1_(}k^4k7oi>$`Ao7;pnxyPogHU{G=dn${#%7Y2^nMg0T+*ES{kFh<(L!CZ zAy+3^q|DqP=oy9gV)WK9&jdJ=xzNL{aNE0B2+Gm!@{N?4YXv-mL%5r18_w4%{v&(y z7zlE5dRsYo*znjHDsBBC_brxetdGt*4^dmdFHicP&dwv#91-n@D-kk~L^=AYg?xcQiK zebtt7qYbs3UC!1y=x6xIMD3(}fRGhaM-OjP-;g?|L>hQl5MJEHj&HD;E&CUYQ2SPw z+}hN{k|ibW#N3p_bJ%W@n|56OKC+G0g@ zm1Di1bF1iXjHa7%mlRx5LUKo)0hggb$5%z{=gvxLv2$*y8dX#;S|>TRfvb}$kyHna z$zi?7MW5{Ng4KTK*ZZQ)sZeSj(1-n)lhfeeGjpgXGpCdX?aays$<30B?>*1!_H=V- zYp)yl`LTHTgK41ARal4N+1b}Jw@FhD>>qdaRqHzyX^JU;(bx-6cMXNZz^Tb$1x(ru zHzqcE%{BiMk4eUt&QevxImkNU7>l^5_fBV~qn|?B;A!yJ+_e>E%RWZZ^@}W+PE!ON zBZWE9d#T9{p4@m|a39NlMO4)#|8!u?3zKY7NZf0gP7*S<>;-Kk4l}p2SeMTO_f%0} z-rf3yQ#)sBj~nX#GKmP}X=d7T?1J$&kT!4tVmm#4J-I#HxV)$b+tj!KjO~XRu5l*4 zD*?lCscxO62c+*in_)z(ma{Nc?v?Xd)?$_>qFG&%uAcbj6*qp}P3z}0IZgmBA(h=t z8ZK+ub$K~j*IfWp2yA$#-Gs5*?Q0KMDLzOyp1@R zHAFpFbJvZb|5N>O6c-K`F(W>hfNXJ-uwDTNaV6u)KW&X{jkM%>GvWO-HcilxIX5t& zFYoVDGV}UQoap`_ny~rY$M+T_ER%V0*`~^GWh_FypXRG zmaLG7P?>242a1ohyiPoa6CoV8OU8hF5X&9+)#tbWwF#nvK>VoE(Y8L>R;Y|9Zx?~6 zZR!XXbKZ6-q?{xXk~b3uf^_#08r zFCvd2Y;($6-X$k6IcO;h9f)xxgfUyu59EE4%C6cit%zGsEE7FM#?hHDpO&` znUlMxZpWs4hc7Ahh}k-!;=~(MZxqcgmZMuwm)b&Gj}cHPD$|L~fX&3NN6RfEO*WaZvX#Wgwq(lAm%hrB@A`HFw?ePJ!U z;kx6CHQbfw4f`gyoZ#~0Pl>HlFPiHjhNqh%R|zn#TJB5V$E3r52)YJ~FpM`<9|NF} zUOX>NHGuW!;BwKMdltx=RE6S{bQJt6fNds`95)I^UzTCp`IuwJ0H=SnGj;jEr6$=s z1>7&(Btm@dL(A6eMV@tOix`h~e1S!jA0hd6$-P1|eiOhc+6_XM6 zVQeoxEByI)h&{q3t%I z+$j4>VsGeDL}o?2ozwQEnbxT*BOHj$Vc#k@S9>z)D0n`fwN+X9Oz(}*&6 zg5SIDtx}`HsZiS_*J?#W^!61f8psbM={4v=oi2@Ta0pz&D>{qU*tu^9koti zl-?`Jd>LtKz5z6aMg6cgr0Mc{wi2gZ8QnbzDUM8MONm5BP?q6)uCxoU@MoWz9>?|^ zfzFyO$6c%kA59!DK1c0z4etmw5Vt*A-pDjFDbD|8&K{4C7mS3R*`IDA#2*uzG$E}{ z$g}WQpE-Zfgn0jVe(*^g9Jcen`V9lRt%u+~$D2WZ8!{ua zu~{c$zL82z1`7g4iW1{v5Nu04^QKoPNdzXUiaYZzItfC2`|YAwRYNl}L*&6I330~+ z6K2UqzESOa`MSY?QcMx6KO0-=KY$(;oq(VHE&LEg2VoZaA>rxs zgpe<(v}{0tHp%#2{#HyNsd3XE%n_~XEOK4DwImMtHD$AGIP)q_6e>nplUmGE1dR8a zQi0v1KTQp_1p0GwDR)$DOmYB+YF8C2f{85%7%VO}v4m{&zm%%hUYQsK4!sNOB2SYF@;a%&wUAE-(ef%2Y#KpYeiwwoEcf6vHTwHryiT6LGIex&vzKy}9)zYZ)VCy&FIGxDBVLciqi8 za|wtu=M5&;lImRDqWA-DhVwE%Ho8c?_Xm6WqPgt54-1xAOs>Y4jjP;6nqOCYav7Mr zyt-hr*-)dNKDW|Kxxadb9E^huJ%^KD99RY{sZ-b*kQZJ%A%VDjf8ervjZwLuTz~y< zP;?sTonY?s2Fo11_OKL6@)yR6mEg0Mq#m>(R@(==edP=P0v-g{Hif00d2e$SXyvJO z36F*{@)Drxlo_S%isKVVifN&vUV@*CuCfk$NNP;f(*m}S#K1t&{ki?Ub(5}xvrr7& z<)N-MN>i7~ZE6ikz=oRk1=#tG0U^2)x(P097CQtIX9O;Y<^I6Np0H17mj@0#EffM~ za;nUUFOiRn1}mJTDL_ae4w9sXGdInW4VoJ#33}(K08*0xb~@Vd`f*&!%x(K@D0>Aw zn+a8dE6*j~Q;Cf`$n^djV2o_#bi!_>7$QwlvyciO-P{^|Eujhzv5#__iVPX7(AGXX z7Lup{Q>Jzm?^na~D3n?gEbi`aGSlRU(3y&Ts7z7EUu57~P=UXDOQG&E!P}eS6CzL> zr6Lz+ou>md8HA7Iz3?f2iK2JH>~4=b1oT&cm%+c0jKC5ejx?oOx9_$_G)rl(q7rnP zH1aZw*IZz#?qOn5Lh4gc$~o)v0_mLeXr>y>8(*=fEOnooH$2RknuVk%US;agHd8GMhG!)?cdImd6c<*>)JPC7t}+dmkWbDDW)$<)maqZ4@tpkKY1pKyFvBgGoU zldRyB)G2;IwO?v?WExtaYL5z1fet$ZejE=Aek@=c3s$GdNtaM;GE|8nvFj za^!@vXm>cq#K7Qjh&UnXSmY&<=gI0jd76QdV#Y!%(p1DlSb{)nSm#06?Dsj(R;8~F+4W_PQ@ekeF3daxe2d& z2RZ4XxN;4xqwfSEaZ=v5WPHUEoC=^+W*F>&iP=gWBz;b1nsV9(S=u)Ndt%GWoH5mB zOR37PZdrb5XPZNU@VcECZGKohkl1(wBY>TZ^;K<}rmxuVks_J2@nzl#PS>e-a3t^@HEl zb@Th|j%E&2@ZX!pxh4v>Ry!-wuf&I1e2l+0(5K!<6Z++Ku54XHQwE>r4T6z;1&xC) z7=i+0ljh#L4W~*@;OsDqp%!_|)+t;6;S=6Oo@cTW0~)RAd^y1!{M_UC(eKW)8>nN% zWtw!*!fjZVjgRoO`qfSKq#R}TAnHqPBfE`C zO4L6Gn;hMWI#_-jEXZapCpN})ySgZDt>V(CU`v9*!}&(<2Bt*r@9!^g`qD_SZyEs# z#ldIgR$kqt&rQ19o^~Kh4fm4Bi~lapW&3?c4wx{Ptu`RY$S%cN9Hbq}x2!8_mupKZ zOKeVU1t759SEn>M8Qy9apo{e3f{TP1X_|t!wUp&{7usC9>vt4V!`UWLTak=}+B_S! z^YZeumDL#Zb$w+KY3^IpxGN+l z_lSVm(t``FK4gD`1K$ahOmC-+5vR30a65ES&Dqp$p+4=(%&Spn9v98}18k1>L!x9ov>ydAZ59si-CJs9yJPlhsRfxh!V~ z2I_QRbgDA|L&?d>?pnB{J^mpFQ~xwaa!N-P(bMid7!7{xOvz(%qpRUAMub#0h)3+1 z=vf5h#LUnG71S_k<$^z`&UbY;wzk>*Z==bpVT;Tf--+vFpTe?4*45rgGdLB5;V!A(e)Vu<&PneMj zkbPqP-qH?A{5`)MKDzx=(8{GD6Rd{UHju+*?_;bI)_PjT@BH+R40QbP<822!4>0^5 z4D@|>2nY5A2eN~QF?@{JgE}2=Lv^_9L0;PjvG`o}?-AXzcI^DJLkL_39@_)h>3!H1 zF0dP&VuBYP&qa{okS;4SQys%z1#{<5izs#*Z!?TuM7{;s>ph#T_raG*4?1^%@r`Ka zPKPZ0Jf_zI(uLFN@+VT~J6wKI=DQmOeP4)dIu;YNY;FoqjbXorT<;HWwYkkp-&5EY zknz<6O84p0=7h0%;HJ;T%4I4RCR~sI>3$J2OxW37==gutSz_zqqm=ip#Re|Ft66WL z_v?yzp07Kz-p5&k;1l>p04ZONO<#|-H<_~`S*-st_E z_){*30NS0R=jA^6c&+Rd#6%iW7F)9Qduy1G67{sFFRcMkwo;e zhKQgNV^L#vB4p)iMep26#{N9&?|ls*Y17Z1jxX?HCi^2?ptqUA&xJ)RD401lj&ghnb)?g`^^gZLsfu`F+Jy|<^;3V&(hv2 zYngb%AkdN^Wl;B6<_tpib_1WV+OcuX608S*5aOw;43?g&Qe}?mU=)z%P^Df2t?hgs zoW5wY2tft@vteaC3brE^Q3E$xgyr!jn2SM}!vcF5p^W3=_?|nCi8W9H%!??@av#UA zso@B}9h}9M?{KmSCV32e%tnCsdz86&z}uwPEBz7 zK%#qC2VU*g*EKCsaeK1@#_Cy7Uf@XS`DlSe3G{d)lj6Z&-ogyXQ#axw{ALewh9iOn zp?aifd<$?=jLzmGx^IUcVoxhnLo${RUNBM{dx7C9oY2ro1)NhFKgIP4n4l+(c@YHQ z?EoaU@K8)|8Q}^p&z05rRbBJYL%g zRdE9gWQXDD&L(VW)0nmI`K?4A4asT6_9U#+vx-mt_K|u|O7g|5&_1nG5*vowSU=_} zEcz!BEUq14MMVY7rmredY}U-X95pvn74Axki$wG+h?j==CI<~(X$udvZB{e(=#-lX z|5dN@kOCArn&nW!GvNd7q@aV8o>P0s&9#~Ya#YF@Ui8>{JZVn8k!Qah8KtjLJ3lS$ zQL_DJ#fGLywN=n-Z*O)gFrrzuq`TqglutcL2E;8Maf}}~`(T`qLS_4W6`3Gk8i%vu z0m;>Ubz)?&sDcpnLyj2hHjjorp6=Bp$%O$+SET+Td=uQL1tsydg}0NJ(Cd9lA>+>v zLF(21(KHVgN+7k!`ibx5%+}QFsfc|0iPr)yse*3vsF^2 zoU+4VB~-uE?YDuIQMP)QUzOuB_oXwZx|6mA znsHVzXtx__RV7(vJ^vD@hfT8( z=vp7k`;%i>r?W zoizm>&x`41pc9Ir)%{pyBu;RRv5AbuJA?pN7S1{x_0yew*;8K$#%NpRyc+3w{gyxu0CEvW>Bm|G5Yra#nCjU<52y2 z64sG#E1u5IM};fd(P}~|oEbe9oXgJ~R~V4s&Z}%bMs?CzVaeZTSFdDb$x)1ZDot2# zYIpEs#n9{+K$8qqNj*5mm^`q-9D4;2ms{b}lVm}5-1*24&U>ag)>ggvM07~1$J%1G zI?0`d^>cuEQ)w^o9jtdatt#k+koKdVhEV}HF_8;bKUTUp1p@8D$4p>E1}uJAyC$0a zR;FD@Z0(|1rU&m3!IsPr0_?J#F4T&{t1MW#P8y*F;Xg`Wo%cFruCg|w+182{;`K;x z3R`MF6|!li04mq42hl8DDzXZp#0k^DTqLUrA2zkb``&-*B)N))4=&7`#UMg?Z2$4ZpH1PO z4gr@Rj1ZKn;MPPgFxQ54k%lID!T1MO{oVAWhN?7&cv=RXO%w-9(x|5S4;mqrJUk@J z>Esgd=@4@G{mx{4)@|iCLcHh;513)bhiD>a)@ForYZ$zQ&Fx|;VoHs`g5j_m==s3k z`$VI$>|@RU^?>U*+8C1mi zu}}MwOS{~|CU&=9QI>mI{)wgM#_|dJybG^aeo3w1Sv@LawSMDz`PdqJdY;?5`_Gz* z_x=I>T^?T!JP4^7c&M#cDj=wa>#OR@Y{_NnI{@&I$Xk)i7zZn3f;NRh{ z-W@NrY`SMpPG7IZHx44<)MF6W3wl2_JbF9xB2a=6q`E4n$h{>4x|P!`+p zgz6CV+Y3Q3RqSJX3inzIIj$SP%0Bg6^!I3~2&V4IgV+|nwEl^kSXaB~{<=Y4Vvyns zVnc-${^v4-I0RcICaPcVQcWoR7CZbNsJ&U$iI834DL)%?98F{ z8+16g)NGO~qfpCo)aHO4^lQ<@cg=*bJ1K-ga3*j8-D+zpW8x{^&|hWNuuW zp+Itr?0TMw&25L}(^F>GvB>vJ#{h8xpUYmf&-~(pT5Z8=__JsjngUzYyVDW8rGn)#gCO*Fzfv+J94iuqbUSsFt_wHWh;OF3o%r}b` zi$?=1!54_et(H2F(cRqYTjPu}QP3#1Wb*)<-o43uryo>(!fbDIMiS@}8|zlFZ2n4K znt`|}MOh){bQpY)!7;}PVweEnBR>3xq9JFOIv*^9ho2A*aSOFBy>Qa3lE@a#6`eDvdS@5y@YQ7P%vY+-#J?pdHx(I9spU#Gw&a>{Zk);0%Eg zjGsLCWkL9xJaM|yBD4Zk=alaN!^^c)7! zQF#kn9|RQ8ZDZ9~ZtFMByRMkgsR;tw*Tr?WR&-4$5O@U!9B?gtQLaSa5JUb+f9}Ec zd7^H+U}d~XL+c5XV?KesOW%)KUrmjn_;^->6%ZOhzo$|b_X{)nsDHJVa( zfdxOSRNHIaZ03_gYH2-&7m+bNS=W3aQsW@jfSfi-RP|-GwU=I!yl~KDoE`J_K2$lF zcs>brTQtn{S_r>a4n+^h4M=#W1ceciD86lwiX`2fk&>^BMoUbP+Pf$;>{eOquC<>& zK1dHSb4MkwjPvPwtQTw+!NVxsw)bzSx0d|(iD(S8}jU*<~r~`cD_2$^-zIg12X&-6m){MAnhZ2G;tu54wAE2 zB7Ion_=kYTg@xoe_>aH3W|GfZQZ7_+zmm+H<=`1ct^=fgN^9KdZeZ#H@J^gZUiQWj zFf0<`0ba1^gke}80nw`jmg!MUTS)UsK^q>dwkpV-dVd56vO^NIu8MSib<&%Zy9L_# z%}dJBz?t}|lMt?CR5oKnuR&RgFd`7{tHB4=L|SX-cT)MYYxNM_mAP@NyNB2n;Lt{= zYQtg``Fds7j96t(jyzJz=Wsn&)%-BmGW~sGW9XipiH!84QUVB(@@xg5sc0&XLZ^#{ zJwHs7<%Q1iIcGDqujK@&>`8Qd?cFmS#>C&198P|BHgtgv$$+qW{tO` zzSV`a=J&jpOVvZ+;Q*3_TsTsh(3Eb(EH>F^m=M0(D7esW?V@xL;ucsm47fPn^?gwJ0tE1F*94}J@s6FcNBK1(Ku zM?I;W{x7NTBqgNc7tc3+t!`ZUhtlS+6yfsEoP_n?_JZR17VV%b?tgYFO4y(k#qkc$Luw^e_ zFgQFKjTK8&kcbz_6q+6^q`(&uQHDY28e|-d!s(-pua5hsXapU<783n5a7gs)Ln!)d zLliZ5oXyjp3O2O54-c&~W}XZN4rZ4@q9=#*J2{}E;|E3dT>6tJcPJiEZ-QraKN)!? z6OI7Anh*h(6vcJ#-r^hLclB04X;~$~MnJ3b^ly?%^qqZ#nG)?@4@p4}+QY&Peg7Ii zfm{P&sU~ng_Txn7WBB~~b~>{3^-FH9Ug2X(HAv+C2rXswYDlUUcB4U_XA?CKJv}UG z=H&5`C-ZY7#O${O{$k*`aqjKFZ4H!KbVGZf^_;ilt~%GgqEXbajcbHVxNIV;SQD)c z04;3|?@IKQyUED%F3O12{Byx2&iI<9_BjM8T-n90r(M^(Yu;67Z`-IKG!9hUhx>Tsij6-M_^{-gTZpPnJi+vxQND8EvcReDgOT(!+ExN#%mQQ?HCN!i3Ht+P9Q@7E_# zm$PB9>_6FdFV;=uo%tR59lD~w`-=@ucGILkNiTk6xeA4?RgrLn{}mE4f?esAU^^I= zlA&$-*O{pm_+HYeTwlz8M%uC8t_(;jn&wCf4oTEKL5a?)7j=QI!5gn6Zll{y!wo_A zg|{^6X2C@BMl|J4pqOm9yeO>1#B|XYgK?UD>t5EAcNn(mX0v*s1H(BX#U2;xsY}`= zw!McYre|e%KH@U3yqtX1vj?yz<;?W3RpTC<@|m#4ON5ef`Ula) z3>_J%&R}gEwN$xJCr7Uz^IHp9+i4FcRb({DTw;6bPW~ixf){q{@sZ?g5*9oRTp1JT zBhzrb%miNEl;mzX%~R@QAZ*M*i7V`4X?z>S6uK67arG8k+}5~OSLV}ZP*w&YiD!C4 z;&eIaIu_YPfOu@v?~rs|;=eSSG{XQL2`?M#(?NY!SZJGSkUl>qq8qRB&=We)W?2>t zZ%q+!<(chy*ZXbs-SoYzBq_C||1d(H?S;o!C}b9SO~5ISRg-b~uITs8 zi#FV@54eI1@Ap$8g}voRbIKg=;iZWAnJEyW_y3qIbf(UsGRjRqTHIiXAI?v#L4kR^ zMu-^Sht4`3_$H^2OBIv>d83=is@s58Q^T7eKQ6JYbgdi=8}usC!)G$+WISDvj#78+ zPi)&}TjGl(OZlH^AFaxQ!wnW9;$E=M?W}d*H67;$g7?mR}r=)l_Go;`|vwiNnwQgQI<1&eX$ABq>hp zPjHB`>0Xxyl2H$BdM>8sW8%q^{x-zV%I*EEyIRi{m!BrI`yw-2>0Ih1Rz9j~KP9GbHj+|gmfo^_%K%eiYUN{gZS@!s7^ zUUvAyQSP?@377RH(#q$v|NGMCY0qWH+!OHgzGt@jb99}3qDaXs!cBEp*O(7?{AcMW zr1V4d(M9mdt~Ieeq0WxreD-?6Z=k^IE~weO$Mf?->e~wOlmt48C_1_S!{L_Q@XqJ! zr*G67@#kThg2S-B=W&bQ`tlq#rOCJWpEoO-@o&(6Tqatd@BVL>D!cxEH>7KTZ9>2b zFIIitR|=x#bU*K>E19r(^4swr!6-+{PZTB8TlojUkHh0@sqnLH!B=TbPnx&yPx5Y1 zD#wg(=Wmh&O%FJCi%2psiw%H;EO{;Bvg1M&5h znW#wg0=fHPey+z>!I#<(JKIO=bNiD=>|^XRGEiZ5(NWU|3i$cJ|4lx0qeVUu7@(lO w#f?h;fdM8To~g3u2=>1d@c-YF9HLL=<_z<>I`S>k|FGw!#N{a.d(t,{Zo:()=>p,kt:()=>h});var n=a(67294);function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function s(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function r(e){for(var t=1;t=0||(o[a]=e[a]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var l=n.createContext({}),u=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):r(r({},t),e)),a},p=function(e){var t=u(e.components);return n.createElement(l.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,o=e.mdxType,s=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),c=u(a),d=o,h=c["".concat(l,".").concat(d)]||c[d]||m[d]||s;return a?n.createElement(h,r(r({ref:t},p),{},{components:a})):n.createElement(h,r({ref:t},p))}));function h(e,t){var a=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var s=a.length,r=new Array(s);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var u=2;u{a.d(t,{Z:()=>s});var n=a(67294),o=a(44996);const s=e=>n.createElement("div",null,n.createElement("p",{align:"center"},n.createElement("figure",null,n.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,o.Z)(e.source)}),n.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>r});var n=a(67294),o=a(39960);a(44996);const s=()=>n.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>n.createElement("p",{className:"in-blog-cta-link-container"},n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},48238:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>u,default:()=>g,frontMatter:()=>l,metadata:()=>p,toc:()=>m});var n=a(87462),o=(a(67294),a(3905)),s=(a(39960),a(44996)),r=a(92908),i=a(70589);a(38610);const l={title:"Feature Announcement - Wasp Jobs",authors:["shayneczyzewski"],image:"/img/jobs-snippet2.png",tags:["webdev","wasp","feature","jobs"]},u=void 0,p={permalink:"/blog/2022/06/15/jobs-feature-announcement",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-06-15-jobs-feature-announcement.md",source:"@site/blog/2022-06-15-jobs-feature-announcement.md",title:"Feature Announcement - Wasp Jobs",description:'You get a job!Storytime",id:"storytime",level:2},{value:"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05",id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-",level:2},{value:"Real Example - Updating Waspleau",id:"real-example---updating-waspleau",level:2},{value:"Looks neat! What\u2019s next?",id:"looks-neat-whats-next",level:2}],d={toc:m},h="wrapper";function g(e){let{components:t,...l}=e;return(0,o.kt)(h,(0,n.Z)({},d,l,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"You get a job!",src:(0,s.Z)("img/jobs-oprah.gif"),width:"300px"})),(0,o.kt)(i.ZP,{mdxType:"WaspIntro"}),(0,o.kt)(r.Z,{mdxType:"InBlogCta"}),(0,o.kt)("h2",{id:"storytime"},(0,o.kt)("strong",{parentName:"h2"},"Storytime")),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Storytime",src:(0,s.Z)("img/jobs-storytime.gif"),width:"300px"})),(0,o.kt)("p",null,"Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you\u2019re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?"),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Spinning!",src:(0,s.Z)("img/jobs-spinner.gif"),width:"30px"})),(0,o.kt)("p",null,"You wouldn\u2019t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset."),(0,o.kt)("p",null,"The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/workers/github.js",title:"src/server/workers/github.js"},"import axios from 'axios'\nimport { upsertMetric } from './utils.js'\n\nexport async function workerFunction() {\n const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')\n\n const metrics = [\n { name: 'Wasp GitHub Stars', value: response.data.stargazers_count },\n { name: 'Wasp GitHub Language', value: response.data.language },\n { name: 'Wasp GitHub Forks', value: response.data.forks },\n { name: 'Wasp GitHub Open Issues', value: response.data.open_issues },\n ]\n\n await Promise.all(metrics.map(upsertMetric))\n\n return metrics\n}\n")),(0,o.kt)("p",null,"Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file."),(0,o.kt)("h2",{id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-"},"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05"),(0,o.kt)("p",{align:"center"},(0,o.kt)("figure",null,(0,o.kt)("img",{alt:"Eeek",src:(0,s.Z)("img/jobs-eyes.gif")}),(0,o.kt)("figcaption",null,"Me trying to lay off the job-related puns. Ok, ok, I\u2019ll quit. Ahhh!"))),(0,o.kt)("p",null,"In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery."),(0,o.kt)("p",null,"In the JavaScript world, ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/OptimalBits/bull"},"Bull")," is quite popular these days. However, we decided to use ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/timgit/pg-boss"},"pg-boss"),", as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack."),(0,o.kt)("p",null,"But isn\u2019t a database as a queue an anti-pattern, you may ask? Well, historically I\u2019d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [",(0,o.kt)("a",{parentName:"p",href:"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"},"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"),"]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective."),(0,o.kt)("p",null,"However, we will also continue to expand the number of job execution runtimes we support. Let us know in ",(0,o.kt)("a",{parentName:"p",href:"https://discord.gg/rzdnErX"},"Discord")," what you\u2019d like to see next!"),(0,o.kt)("h2",{id:"real-example---updating-waspleau"},"Real Example - Updating Waspleau"),(0,o.kt)("p",null,"If you are a regular reader of this blog (thank you, you deserve a raise! \ud83d\ude0a), you may recall we created an example app of a metrics dashboard called ",(0,o.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/blog/2022/01/27/waspleau"},"Waspleau")," that used workers in the background to make periodic HTTP calls for data. In that example, we didn\u2019t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge ",(0,o.kt)("inlineCode",{parentName:"p"},"setupFn")," wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"title=main.wasp",title:"main.wasp"},'// A cron job for fetching GitHub stats\njob getGithubStats {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/github.js"\n },\n schedule: {\n cron: "*/10 * * * *"\n }\n}\n\n// A cron job to measure how long a webpage takes to load\njob calcPageLoadTime {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/loadTime.js"\n },\n schedule: {\n cron: "*/5 * * * *",\n args: {=json {\n "url": "https://wasp-lang.dev",\n "name": "wasp-lang.dev Load Time"\n } json=}\n }\n}\n')),(0,o.kt)("p",null,"And here is an example of how you can reference and invoke jobs on the server. ",(0,o.kt)("em",{parentName:"p"},"Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/serverSetup.js",title:"src/server/serverSetup.js"},"/**\n* These Jobs are automatically scheduled by Wasp.\n* However, let's kick them off on server setup to ensure we have data right away.\n*/\nimport { github } from '@wasp/jobs/getGithubStats.js'\nimport { loadTime } from '@wasp/jobs/calcPageLoadTime.js'\n\nexport default async function () {\n await github.submit()\n await loadTime.submit({\n url: \"https://wasp-lang.dev\",\n name: \"wasp-lang.dev Load Time\"\n })\n}\n")),(0,o.kt)("p",null,"And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:"),(0,o.kt)("p",null,(0,o.kt)("img",{alt:"Architecture",src:a(81971).Z,width:"2626",height:"1452"})),(0,o.kt)("p",null,"For those interested, check out the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/commit/1721371fc73f4485ca0046aafea2ee3fc0be41cf#diff-e158328e137176b595ad01641ba68faf82dbb88ccc5be3597009bb576fcd6505"},"full diff here")," and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!"),(0,o.kt)("h2",{id:"looks-neat-whats-next"},"Looks neat! What\u2019s next?"),(0,o.kt)("p",null,"First off, please check out our docs for ",(0,o.kt)("a",{parentName:"p",href:"/docs/advanced/jobs"},"Jobs"),". There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau"},"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau")),(0,o.kt)("p",null,"In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!"),(0,o.kt)("hr",null),(0,o.kt)("small",null,"Special thanks to Tim Jones for his hard work building an amazing OSS library, ",(0,o.kt)("a",{href:"https://github.com/timgit/pg-boss",target:"_blank"},"pg-boss"),", and for reviewing this post. Please consider supporting that project if it solves your needs!"))}g.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var n=a(87462),o=(a(67294),a(3905));const s={toc:[]},r="wrapper";function i(e){let{components:t,...a}=e;return(0,o.kt)(r,(0,n.Z)({},s,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,(0,o.kt)("em",{parentName:"p"},"Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.")))}i.isMDXComponent=!0},81971:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/jobs-arch-3ebc08ebc717194dfac7e67fca5b8a7d.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[70018],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>h});var n=a(67294);function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function s(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function r(e){for(var t=1;t=0||(o[a]=e[a]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var l=n.createContext({}),u=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):r(r({},t),e)),a},p=function(e){var t=u(e.components);return n.createElement(l.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,o=e.mdxType,s=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),c=u(a),d=o,h=c["".concat(l,".").concat(d)]||c[d]||m[d]||s;return a?n.createElement(h,r(r({ref:t},p),{},{components:a})):n.createElement(h,r({ref:t},p))}));function h(e,t){var a=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var s=a.length,r=new Array(s);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var u=2;u{a.d(t,{Z:()=>s});var n=a(67294),o=a(44996);const s=e=>n.createElement("div",null,n.createElement("p",{align:"center"},n.createElement("figure",null,n.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,o.Z)(e.source)}),n.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>r});var n=a(67294),o=a(39960);a(44996);const s=()=>n.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>n.createElement("p",{className:"in-blog-cta-link-container"},n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},48238:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>u,default:()=>g,frontMatter:()=>l,metadata:()=>p,toc:()=>m});var n=a(87462),o=(a(67294),a(3905)),s=(a(39960),a(44996)),r=a(92908),i=a(70589);a(38610);const l={title:"Feature Announcement - Wasp Jobs",authors:["shayneczyzewski"],image:"/img/jobs-snippet2.png",tags:["webdev","wasp","feature","jobs"]},u=void 0,p={permalink:"/blog/2022/06/15/jobs-feature-announcement",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-06-15-jobs-feature-announcement.md",source:"@site/blog/2022-06-15-jobs-feature-announcement.md",title:"Feature Announcement - Wasp Jobs",description:'You get a job!Storytime",id:"storytime",level:2},{value:"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05",id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-",level:2},{value:"Real Example - Updating Waspleau",id:"real-example---updating-waspleau",level:2},{value:"Looks neat! What\u2019s next?",id:"looks-neat-whats-next",level:2}],d={toc:m},h="wrapper";function g(e){let{components:t,...l}=e;return(0,o.kt)(h,(0,n.Z)({},d,l,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"You get a job!",src:(0,s.Z)("img/jobs-oprah.gif"),width:"300px"})),(0,o.kt)(i.ZP,{mdxType:"WaspIntro"}),(0,o.kt)(r.Z,{mdxType:"InBlogCta"}),(0,o.kt)("h2",{id:"storytime"},(0,o.kt)("strong",{parentName:"h2"},"Storytime")),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Storytime",src:(0,s.Z)("img/jobs-storytime.gif"),width:"300px"})),(0,o.kt)("p",null,"Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you\u2019re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?"),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Spinning!",src:(0,s.Z)("img/jobs-spinner.gif"),width:"30px"})),(0,o.kt)("p",null,"You wouldn\u2019t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset."),(0,o.kt)("p",null,"The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/workers/github.js",title:"src/server/workers/github.js"},"import axios from 'axios'\nimport { upsertMetric } from './utils.js'\n\nexport async function workerFunction() {\n const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')\n\n const metrics = [\n { name: 'Wasp GitHub Stars', value: response.data.stargazers_count },\n { name: 'Wasp GitHub Language', value: response.data.language },\n { name: 'Wasp GitHub Forks', value: response.data.forks },\n { name: 'Wasp GitHub Open Issues', value: response.data.open_issues },\n ]\n\n await Promise.all(metrics.map(upsertMetric))\n\n return metrics\n}\n")),(0,o.kt)("p",null,"Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file."),(0,o.kt)("h2",{id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-"},"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05"),(0,o.kt)("p",{align:"center"},(0,o.kt)("figure",null,(0,o.kt)("img",{alt:"Eeek",src:(0,s.Z)("img/jobs-eyes.gif")}),(0,o.kt)("figcaption",null,"Me trying to lay off the job-related puns. Ok, ok, I\u2019ll quit. Ahhh!"))),(0,o.kt)("p",null,"In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery."),(0,o.kt)("p",null,"In the JavaScript world, ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/OptimalBits/bull"},"Bull")," is quite popular these days. However, we decided to use ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/timgit/pg-boss"},"pg-boss"),", as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack."),(0,o.kt)("p",null,"But isn\u2019t a database as a queue an anti-pattern, you may ask? Well, historically I\u2019d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [",(0,o.kt)("a",{parentName:"p",href:"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"},"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"),"]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective."),(0,o.kt)("p",null,"However, we will also continue to expand the number of job execution runtimes we support. Let us know in ",(0,o.kt)("a",{parentName:"p",href:"https://discord.gg/rzdnErX"},"Discord")," what you\u2019d like to see next!"),(0,o.kt)("h2",{id:"real-example---updating-waspleau"},"Real Example - Updating Waspleau"),(0,o.kt)("p",null,"If you are a regular reader of this blog (thank you, you deserve a raise! \ud83d\ude0a), you may recall we created an example app of a metrics dashboard called ",(0,o.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/blog/2022/01/27/waspleau"},"Waspleau")," that used workers in the background to make periodic HTTP calls for data. In that example, we didn\u2019t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge ",(0,o.kt)("inlineCode",{parentName:"p"},"setupFn")," wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"title=main.wasp",title:"main.wasp"},'// A cron job for fetching GitHub stats\njob getGithubStats {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/github.js"\n },\n schedule: {\n cron: "*/10 * * * *"\n }\n}\n\n// A cron job to measure how long a webpage takes to load\njob calcPageLoadTime {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/loadTime.js"\n },\n schedule: {\n cron: "*/5 * * * *",\n args: {=json {\n "url": "https://wasp-lang.dev",\n "name": "wasp-lang.dev Load Time"\n } json=}\n }\n}\n')),(0,o.kt)("p",null,"And here is an example of how you can reference and invoke jobs on the server. ",(0,o.kt)("em",{parentName:"p"},"Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/serverSetup.js",title:"src/server/serverSetup.js"},"/**\n* These Jobs are automatically scheduled by Wasp.\n* However, let's kick them off on server setup to ensure we have data right away.\n*/\nimport { github } from '@wasp/jobs/getGithubStats.js'\nimport { loadTime } from '@wasp/jobs/calcPageLoadTime.js'\n\nexport default async function () {\n await github.submit()\n await loadTime.submit({\n url: \"https://wasp-lang.dev\",\n name: \"wasp-lang.dev Load Time\"\n })\n}\n")),(0,o.kt)("p",null,"And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:"),(0,o.kt)("p",null,(0,o.kt)("img",{alt:"Architecture",src:a(41162).Z,width:"2626",height:"1452"})),(0,o.kt)("p",null,"For those interested, check out the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/commit/1721371fc73f4485ca0046aafea2ee3fc0be41cf#diff-e158328e137176b595ad01641ba68faf82dbb88ccc5be3597009bb576fcd6505"},"full diff here")," and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!"),(0,o.kt)("h2",{id:"looks-neat-whats-next"},"Looks neat! What\u2019s next?"),(0,o.kt)("p",null,"First off, please check out our docs for ",(0,o.kt)("a",{parentName:"p",href:"/docs/advanced/jobs"},"Jobs"),". There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau"},"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau")),(0,o.kt)("p",null,"In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!"),(0,o.kt)("hr",null),(0,o.kt)("small",null,"Special thanks to Tim Jones for his hard work building an amazing OSS library, ",(0,o.kt)("a",{href:"https://github.com/timgit/pg-boss",target:"_blank"},"pg-boss"),", and for reviewing this post. Please consider supporting that project if it solves your needs!"))}g.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var n=a(87462),o=(a(67294),a(3905));const s={toc:[]},r="wrapper";function i(e){let{components:t,...a}=e;return(0,o.kt)(r,(0,n.Z)({},s,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,(0,o.kt)("em",{parentName:"p"},"Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.")))}i.isMDXComponent=!0},41162:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/jobs-arch-3ebc08ebc717194dfac7e67fca5b8a7d.png"}}]); \ No newline at end of file diff --git a/assets/js/0608e6cc.522f1c8d.js b/assets/js/0608e6cc.92d93b83.js similarity index 98% rename from assets/js/0608e6cc.522f1c8d.js rename to assets/js/0608e6cc.92d93b83.js index b1a055c934..18a338b4ec 100644 --- a/assets/js/0608e6cc.522f1c8d.js +++ b/assets/js/0608e6cc.92d93b83.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[67434],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>f});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function s(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var l=r.createContext({}),c=function(e){var t=r.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},g=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),m=c(a),g=n,f=m["".concat(l,".").concat(g)]||m[g]||u[g]||o;return a?r.createElement(f,s(s({ref:t},p),{},{components:a})):r.createElement(f,s({ref:t},p))}));function f(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var o=a.length,s=new Array(o);s[0]=g;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[m]="string"==typeof e?e:n,s[1]=i;for(var c=2;c{a.d(t,{Z:()=>o});var r=a(67294),n=a(44996);const o=e=>r.createElement("div",null,r.createElement("p",{align:"center"},r.createElement("figure",null,r.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,n.Z)(e.source)}),r.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>s});var r=a(67294),n=a(39960);a(44996);const o=()=>r.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),s=()=>r.createElement("p",{className:"in-blog-cta-link-container"},r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},21145:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=a(87462),n=(a(67294),a(3905));a(39960),a(44996),a(92908),a(70589),a(38610);const o={title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},s=void 0,i={permalink:"/blog/2022/11/26/erlis-amicus-usecase",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-11-26-erlis-amicus-usecase.md",source:"@site/blog/2022-11-26-erlis-amicus-usecase.md",title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",description:"amicus hero shot",date:"2022-11-26T00:00:00.000Z",formattedDate:"November 26, 2022",tags:[{label:"webdev",permalink:"/blog/tags/webdev"},{label:"wasp",permalink:"/blog/tags/wasp"},{label:"startups",permalink:"/blog/tags/startups"},{label:"github",permalink:"/blog/tags/github"}],readingTime:4.21,hasTruncateMarker:!0,authors:[{name:"Matija Sosic",title:"Co-founder & CEO @ Wasp",url:"https://github.com/matijasos",email:"matija@wasp-lang.dev",imageURL:"https://github.com/matijasos.png",key:"matijasos"}],frontMatter:{title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},prevItem:{title:"Why we chose Prisma as a database layer for Wasp",permalink:"/blog/2022/11/28/why-we-chose-prisma"},nextItem:{title:"How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans",permalink:"/blog/2022/11/26/michael-curry-usecase"}},l={authorsImageUrls:[void 0]},c=[],p={toc:c},m="wrapper";function u(e){let{components:t,...o}=e;return(0,n.kt)(m,(0,r.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"amicus hero shot",src:a(98842).Z,width:"1920",height:"1705"})),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://github.com/ErlisK"},"Erlis Kllogjri")," is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how ",(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus")," started out."),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus"),' is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.'),(0,n.kt)("p",null,"Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!"))}u.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var r=a(87462),n=(a(67294),a(3905));const o={toc:[]},s="wrapper";function i(e){let{components:t,...a}=e;return(0,n.kt)(s,(0,r.Z)({},o,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("em",{parentName:"p"},"Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.")))}i.isMDXComponent=!0},98842:(e,t,a)=>{a.d(t,{Z:()=>r});const r=a.p+"assets/images/amicus-hero-shot-5fa944706f38333bf0f22a6784b7fd2b.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[67434],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>f});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function s(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var l=r.createContext({}),c=function(e){var t=r.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},g=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),m=c(a),g=n,f=m["".concat(l,".").concat(g)]||m[g]||u[g]||o;return a?r.createElement(f,s(s({ref:t},p),{},{components:a})):r.createElement(f,s({ref:t},p))}));function f(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var o=a.length,s=new Array(o);s[0]=g;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[m]="string"==typeof e?e:n,s[1]=i;for(var c=2;c{a.d(t,{Z:()=>o});var r=a(67294),n=a(44996);const o=e=>r.createElement("div",null,r.createElement("p",{align:"center"},r.createElement("figure",null,r.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,n.Z)(e.source)}),r.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>s});var r=a(67294),n=a(39960);a(44996);const o=()=>r.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),s=()=>r.createElement("p",{className:"in-blog-cta-link-container"},r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},21145:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=a(87462),n=(a(67294),a(3905));a(39960),a(44996),a(92908),a(70589),a(38610);const o={title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},s=void 0,i={permalink:"/blog/2022/11/26/erlis-amicus-usecase",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-11-26-erlis-amicus-usecase.md",source:"@site/blog/2022-11-26-erlis-amicus-usecase.md",title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",description:"amicus hero shot",date:"2022-11-26T00:00:00.000Z",formattedDate:"November 26, 2022",tags:[{label:"webdev",permalink:"/blog/tags/webdev"},{label:"wasp",permalink:"/blog/tags/wasp"},{label:"startups",permalink:"/blog/tags/startups"},{label:"github",permalink:"/blog/tags/github"}],readingTime:4.21,hasTruncateMarker:!0,authors:[{name:"Matija Sosic",title:"Co-founder & CEO @ Wasp",url:"https://github.com/matijasos",email:"matija@wasp-lang.dev",imageURL:"https://github.com/matijasos.png",key:"matijasos"}],frontMatter:{title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},prevItem:{title:"Why we chose Prisma as a database layer for Wasp",permalink:"/blog/2022/11/28/why-we-chose-prisma"},nextItem:{title:"How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans",permalink:"/blog/2022/11/26/michael-curry-usecase"}},l={authorsImageUrls:[void 0]},c=[],p={toc:c},m="wrapper";function u(e){let{components:t,...o}=e;return(0,n.kt)(m,(0,r.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"amicus hero shot",src:a(32843).Z,width:"1920",height:"1705"})),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://github.com/ErlisK"},"Erlis Kllogjri")," is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how ",(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus")," started out."),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus"),' is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.'),(0,n.kt)("p",null,"Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!"))}u.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var r=a(87462),n=(a(67294),a(3905));const o={toc:[]},s="wrapper";function i(e){let{components:t,...a}=e;return(0,n.kt)(s,(0,r.Z)({},o,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("em",{parentName:"p"},"Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.")))}i.isMDXComponent=!0},32843:(e,t,a)=>{a.d(t,{Z:()=>r});const r=a.p+"assets/images/amicus-hero-shot-5fa944706f38333bf0f22a6784b7fd2b.png"}}]); \ No newline at end of file diff --git a/assets/js/080b96ba.5aa6e358.js b/assets/js/080b96ba.5aa6e358.js deleted file mode 100644 index 2aff99376d..0000000000 --- a/assets/js/080b96ba.5aa6e358.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[535],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>f});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},d="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=p(n),m=r,f=d["".concat(l,".").concat(m)]||d[m]||u[m]||i;return n?a.createElement(f,o(o({ref:t},c),{},{components:n})):a.createElement(f,o({ref:t},c))}));function f(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=m;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[d]="string"==typeof e?e:r,o[1]=s;for(var p=2;p{n.d(t,{Z:()=>i});var a=n(67294),r=n(50012);function i(e){let{path:t}=e;const[n]=(0,r.Nk)("docusaurus.tab.js-ts"),i=t.lastIndexOf("{"),o=t.slice(i+1,t.length-1),[s,l]=o.split(","),p=t.slice(0,i);return a.createElement("code",null,p+("js"===n?s:l))}},94516:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>m,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var a=n(87462),r=(n(67294),n(3905)),i=(n(46300),n(44996));const o={title:"4. Database Entities"},s=void 0,l={unversionedId:"tutorial/entities",id:"version-0.13.0/tutorial/entities",title:"4. Database Entities",description:"Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.",source:"@site/versioned_docs/version-0.13.0/tutorial/04-entities.md",sourceDirName:"tutorial",slug:"/tutorial/entities",permalink:"/docs/0.13.0/tutorial/entities",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.13.0/tutorial/04-entities.md",tags:[],version:"0.13.0",sidebarPosition:4,frontMatter:{title:"4. Database Entities"},sidebar:"docs",previous:{title:"3. Pages & Routes",permalink:"/docs/0.13.0/tutorial/pages"},next:{title:"5. Querying the Database",permalink:"/docs/0.13.0/tutorial/queries"}},p={},c=[],d={toc:c},u="wrapper";function m(e){let{components:t,...n}=e;return(0,r.kt)(u,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database."),(0,r.kt)("p",null,"Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-wasp",metastring:'title="main.wasp"',title:'"main.wasp"'},"// ...\n\nentity Task {=psl\n id Int @id @default(autoincrement())\n description String\n isDone Boolean @default(false)\npsl=}\n")),(0,r.kt)("admonition",{type:"note"},(0,r.kt)("p",{parentName:"admonition"},"Wasp uses ",(0,r.kt)("a",{parentName:"p",href:"https://www.prisma.io"},"Prisma")," as a way to talk to the database. You define entities by defining ",(0,r.kt)("a",{parentName:"p",href:"https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model/"},"Prisma models")," using the Prisma Schema Language (PSL) between the ",(0,r.kt)("inlineCode",{parentName:"p"},"{=psl psl=}")," tags."),(0,r.kt)("p",{parentName:"admonition"},"Read more in the ",(0,r.kt)("a",{parentName:"p",href:"../data-model/entities"},"Entities")," section of the docs.")),(0,r.kt)("p",null,"To update the database schema to include this entity, stop the ",(0,r.kt)("inlineCode",{parentName:"p"},"wasp start")," process, if it's running, and run:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sh"},"wasp db migrate-dev\n")),(0,r.kt)("p",null,"You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database."),(0,r.kt)("p",null,"To take a look at the database and the new ",(0,r.kt)("inlineCode",{parentName:"p"},"Task")," entity, run:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sh"},"wasp db studio\n")),(0,r.kt)("p",null,"This will open a new page in your browser to view and edit the data in your database."),(0,r.kt)("img",{alt:"Todo App - Db studio showing Task schema",src:(0,i.Z)("img/todo-app-db-studio-task-entity.png"),style:{border:"1px solid black"}}),(0,r.kt)("p",null,"Click on the ",(0,r.kt)("inlineCode",{parentName:"p"},"Task")," entity and check out its fields! We don't have any data in our database yet, but we are about to change that."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/080b96ba.d3c0e68c.js b/assets/js/080b96ba.d3c0e68c.js new file mode 100644 index 0000000000..0a99ec67db --- /dev/null +++ b/assets/js/080b96ba.d3c0e68c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[535],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>f});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var p=a.createContext({}),l=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=l(e.components);return a.createElement(p.Provider,{value:t},e.children)},d="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,p=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=l(n),m=r,f=d["".concat(p,".").concat(m)]||d[m]||u[m]||i;return n?a.createElement(f,o(o({ref:t},c),{},{components:n})):a.createElement(f,o({ref:t},c))}));function f(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=m;var s={};for(var p in t)hasOwnProperty.call(t,p)&&(s[p]=t[p]);s.originalType=e,s[d]="string"==typeof e?e:r,o[1]=s;for(var l=2;l{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>m,frontMatter:()=>o,metadata:()=>p,toc:()=>c});var a=n(87462),r=(n(67294),n(3905)),i=n(44996);const o={title:"4. Database Entities"},s=void 0,p={unversionedId:"tutorial/entities",id:"version-0.13.0/tutorial/entities",title:"4. Database Entities",description:"Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.",source:"@site/versioned_docs/version-0.13.0/tutorial/04-entities.md",sourceDirName:"tutorial",slug:"/tutorial/entities",permalink:"/docs/0.13.0/tutorial/entities",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.13.0/tutorial/04-entities.md",tags:[],version:"0.13.0",sidebarPosition:4,frontMatter:{title:"4. Database Entities"},sidebar:"docs",previous:{title:"3. Pages & Routes",permalink:"/docs/0.13.0/tutorial/pages"},next:{title:"5. Querying the Database",permalink:"/docs/0.13.0/tutorial/queries"}},l={},c=[],d={toc:c},u="wrapper";function m(e){let{components:t,...n}=e;return(0,r.kt)(u,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database."),(0,r.kt)("p",null,"Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-wasp",metastring:'title="main.wasp"',title:'"main.wasp"'},"// ...\n\nentity Task {=psl\n id Int @id @default(autoincrement())\n description String\n isDone Boolean @default(false)\npsl=}\n")),(0,r.kt)("admonition",{type:"note"},(0,r.kt)("p",{parentName:"admonition"},"Wasp uses ",(0,r.kt)("a",{parentName:"p",href:"https://www.prisma.io"},"Prisma")," as a way to talk to the database. You define entities by defining ",(0,r.kt)("a",{parentName:"p",href:"https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model/"},"Prisma models")," using the Prisma Schema Language (PSL) between the ",(0,r.kt)("inlineCode",{parentName:"p"},"{=psl psl=}")," tags."),(0,r.kt)("p",{parentName:"admonition"},"Read more in the ",(0,r.kt)("a",{parentName:"p",href:"../data-model/entities"},"Entities")," section of the docs.")),(0,r.kt)("p",null,"To update the database schema to include this entity, stop the ",(0,r.kt)("inlineCode",{parentName:"p"},"wasp start")," process, if it's running, and run:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sh"},"wasp db migrate-dev\n")),(0,r.kt)("p",null,"You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database."),(0,r.kt)("p",null,"To take a look at the database and the new ",(0,r.kt)("inlineCode",{parentName:"p"},"Task")," entity, run:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sh"},"wasp db studio\n")),(0,r.kt)("p",null,"This will open a new page in your browser to view and edit the data in your database."),(0,r.kt)("img",{alt:"Todo App - Db studio showing Task schema",src:(0,i.Z)("img/todo-app-db-studio-task-entity.png"),style:{border:"1px solid black"}}),(0,r.kt)("p",null,"Click on the ",(0,r.kt)("inlineCode",{parentName:"p"},"Task")," entity and check out its fields! We don't have any data in our database yet, but we are about to change that."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0a3b3433.8f99b470.js b/assets/js/0a3b3433.8f99b470.js new file mode 100644 index 0000000000..b9987c29e6 --- /dev/null +++ b/assets/js/0a3b3433.8f99b470.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[77495],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>k});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,k=u["".concat(l,".").concat(d)]||u[d]||m[d]||i;return n?a.createElement(k,o(o({ref:t},c),{},{components:n})):a.createElement(k,o({ref:t},c))}));function k(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[u]="string"==typeof e?e:r,o[1]=s;for(var p=2;p{n.d(t,{Z:()=>i});var a=n(67294),r=n(50012);function i(e){let{path:t}=e;const[n]=(0,r.Nk)("docusaurus.tab.js-ts"),i=t.lastIndexOf("{"),o=t.slice(i+1,t.length-1),[s,l]=o.split(","),p=t.slice(0,i);return a.createElement("code",null,p+("js"===n?s:l))}},87587:(e,t,n)=>{n.d(t,{Jp:()=>i,aH:()=>o});var a=n(67294);const r=e=>{let{color:t,children:n}=e;return a.createElement("span",{style:{border:`2px solid ${t}`,display:"inline-block",padding:"0.2em 0.4em",color:t,borderRadius:"0.4em",fontSize:"0.8em",lineHeight:"1",fontWeight:"bold"}},n)};function i(){return a.createElement(r,{color:"#0b62f5"},"internal")}function o(){return a.createElement(r,{color:"#f59e0b"},"required")}},15954:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>d,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var a=n(87462),r=(n(67294),n(3905)),i=(n(46300),n(87587));const o={title:"Type-Safe Links"},s=void 0,l={unversionedId:"advanced/links",id:"version-0.12.0/advanced/links",title:"Type-Safe Links",description:"If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.",source:"@site/versioned_docs/version-0.12.0/advanced/links.md",sourceDirName:"advanced",slug:"/advanced/links",permalink:"/docs/0.12.0/advanced/links",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.12.0/advanced/links.md",tags:[],version:"0.12.0",frontMatter:{title:"Type-Safe Links"},sidebar:"docs",previous:{title:"Configuring Middleware",permalink:"/docs/0.12.0/advanced/middleware-config"},next:{title:"Wasp Language (.wasp)",permalink:"/docs/0.12.0/general/language"}},p={},c=[{value:"Using the Link Component",id:"using-the-link-component",level:2},{value:"Using Search Query & Hash",id:"using-search-query--hash",level:3},{value:"The routes Object",id:"the-routes-object",level:2},{value:"API Reference",id:"api-reference",level:2},{value:"Link Component",id:"link-component",level:3},{value:"routes Object",id:"routes-object",level:3}],u={toc:c},m="wrapper";function d(e){let{components:t,...n}=e;return(0,r.kt)(m,(0,a.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"If you are using Typescript, you can use Wasp's custom ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component to create type-safe links to other pages on your site."),(0,r.kt)("h2",{id:"using-the-link-component"},"Using the ",(0,r.kt)("inlineCode",{parentName:"h2"},"Link")," Component"),(0,r.kt)("p",null,"After you defined a route:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-wasp",metastring:'title="main.wasp"',title:'"main.wasp"'},'route TaskRoute { path: "/task/:id", to: TaskPage }\npage TaskPage { ... }\n')),(0,r.kt)("p",null,"You can get the benefits of type-safe links by using the ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component from ",(0,r.kt)("inlineCode",{parentName:"p"},"wasp/client/router"),":"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-jsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},"import { Link } from 'wasp/client/router'\n\nexport const TaskList = () => {\n // ...\n\n return (\n
\n {tasks.map((task) => (\n \n {/* \ud83d\udc46 All the params must be correctly passed in */}\n {task.description}\n \n ))}\n
\n )\n}\n")),(0,r.kt)("h3",{id:"using-search-query--hash"},"Using Search Query & Hash"),(0,r.kt)("p",null,"You can also pass ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," props to the ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},'\n {task.description}\n\n')),(0,r.kt)("p",null,"This will result in a link like this: ",(0,r.kt)("inlineCode",{parentName:"p"},"/task/1?sortBy=date#comments"),". Check out the ",(0,r.kt)("a",{parentName:"p",href:"#link-component"},"API Reference")," for more details."),(0,r.kt)("h2",{id:"the-routes-object"},"The ",(0,r.kt)("inlineCode",{parentName:"h2"},"routes")," Object"),(0,r.kt)("p",null,"You can also get all the pages in your app with the ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-jsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},"import { routes } from 'wasp/client/router'\n\nconst linkToTask = routes.TaskRoute.build({ params: { id: 1 } })\n")),(0,r.kt)("p",null,"This will result in a link like this: ",(0,r.kt)("inlineCode",{parentName:"p"},"/task/1"),"."),(0,r.kt)("p",null,"You can also pass ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," props to the ",(0,r.kt)("inlineCode",{parentName:"p"},"build")," function. Check out the ",(0,r.kt)("a",{parentName:"p",href:"#routes-object"},"API Reference")," for more details."),(0,r.kt)("h2",{id:"api-reference"},"API Reference"),(0,r.kt)("h3",{id:"link-component"},(0,r.kt)("inlineCode",{parentName:"h3"},"Link")," Component"),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component accepts the following props:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"to")," ",(0,r.kt)(i.aH,{mdxType:"Required"})),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"A valid Wasp Route path from your ",(0,r.kt)("inlineCode",{parentName:"li"},"main.wasp")," file."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"params: { [name: string]: string | number }")," ",(0,r.kt)(i.aH,{mdxType:"Required"})," (if the path contains params)"),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"An object with keys and values for each param in the path."),(0,r.kt)("li",{parentName:"ul"},"For example, if the path is ",(0,r.kt)("inlineCode",{parentName:"li"},"/task/:id"),", then the ",(0,r.kt)("inlineCode",{parentName:"li"},"params")," prop must be ",(0,r.kt)("inlineCode",{parentName:"li"},"{ id: 1 }"),". Wasp supports required and optional params."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"search: string[][] | Record | string | URLSearchParams")),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"Any valid input for ",(0,r.kt)("inlineCode",{parentName:"li"},"URLSearchParams")," constructor."),(0,r.kt)("li",{parentName:"ul"},"For example, the object ",(0,r.kt)("inlineCode",{parentName:"li"},"{ sortBy: 'date' }")," becomes ",(0,r.kt)("inlineCode",{parentName:"li"},"?sortBy=date"),"."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"hash: string"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},"all other props that the ",(0,r.kt)("inlineCode",{parentName:"p"},"react-router-dom"),"'s ",(0,r.kt)("a",{parentName:"p",href:"https://v5.reactrouter.com/web/api/Link"},"Link")," component accepts"))),(0,r.kt)("h3",{id:"routes-object"},(0,r.kt)("inlineCode",{parentName:"h3"},"routes")," Object"),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object contains a function for each route in your app."),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="router.tsx"',title:'"router.tsx"'},'export const routes = {\n // RootRoute has a path like "/"\n RootRoute: {\n build: (options?: {\n search?: string[][] | Record | string | URLSearchParams\n hash?: string\n }) => // ...\n },\n\n // DetailRoute has a path like "/task/:id/:something?"\n DetailRoute: {\n build: (\n options: {\n params: { id: ParamValue; something?: ParamValue; },\n search?: string[][] | Record | string | URLSearchParams\n hash?: string\n }\n ) => // ...\n }\n}\n')),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"params")," object is required if the route contains params. The ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," parameters are optional."),(0,r.kt)("p",null,"You can use the ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"import { routes } from 'wasp/client/router'\n\nconst linkToRoot = routes.RootRoute.build()\nconst linkToTask = routes.DetailRoute.build({ params: { id: 1 } })\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0a3b3433.995f1851.js b/assets/js/0a3b3433.995f1851.js deleted file mode 100644 index 490ca59ee1..0000000000 --- a/assets/js/0a3b3433.995f1851.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[77495],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>k});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),u=p(n),d=r,k=u["".concat(l,".").concat(d)]||u[d]||m[d]||i;return n?a.createElement(k,o(o({ref:t},c),{},{components:n})):a.createElement(k,o({ref:t},c))}));function k(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[u]="string"==typeof e?e:r,o[1]=s;for(var p=2;p{n.d(t,{Jp:()=>i,aH:()=>o});var a=n(67294);const r=e=>{let{color:t,children:n}=e;return a.createElement("span",{style:{border:`2px solid ${t}`,display:"inline-block",padding:"0.2em 0.4em",color:t,borderRadius:"0.4em",fontSize:"0.8em",lineHeight:"1",fontWeight:"bold"}},n)};function i(){return a.createElement(r,{color:"#0b62f5"},"internal")}function o(){return a.createElement(r,{color:"#f59e0b"},"required")}},15954:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>d,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var a=n(87462),r=(n(67294),n(3905)),i=n(87587);const o={title:"Type-Safe Links"},s=void 0,l={unversionedId:"advanced/links",id:"version-0.12.0/advanced/links",title:"Type-Safe Links",description:"If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.",source:"@site/versioned_docs/version-0.12.0/advanced/links.md",sourceDirName:"advanced",slug:"/advanced/links",permalink:"/docs/0.12.0/advanced/links",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.12.0/advanced/links.md",tags:[],version:"0.12.0",frontMatter:{title:"Type-Safe Links"},sidebar:"docs",previous:{title:"Configuring Middleware",permalink:"/docs/0.12.0/advanced/middleware-config"},next:{title:"Wasp Language (.wasp)",permalink:"/docs/0.12.0/general/language"}},p={},c=[{value:"Using the Link Component",id:"using-the-link-component",level:2},{value:"Using Search Query & Hash",id:"using-search-query--hash",level:3},{value:"The routes Object",id:"the-routes-object",level:2},{value:"API Reference",id:"api-reference",level:2},{value:"Link Component",id:"link-component",level:3},{value:"routes Object",id:"routes-object",level:3}],u={toc:c},m="wrapper";function d(e){let{components:t,...n}=e;return(0,r.kt)(m,(0,a.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"If you are using Typescript, you can use Wasp's custom ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component to create type-safe links to other pages on your site."),(0,r.kt)("h2",{id:"using-the-link-component"},"Using the ",(0,r.kt)("inlineCode",{parentName:"h2"},"Link")," Component"),(0,r.kt)("p",null,"After you defined a route:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-wasp",metastring:'title="main.wasp"',title:'"main.wasp"'},'route TaskRoute { path: "/task/:id", to: TaskPage }\npage TaskPage { ... }\n')),(0,r.kt)("p",null,"You can get the benefits of type-safe links by using the ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component from ",(0,r.kt)("inlineCode",{parentName:"p"},"wasp/client/router"),":"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-jsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},"import { Link } from 'wasp/client/router'\n\nexport const TaskList = () => {\n // ...\n\n return (\n
\n {tasks.map((task) => (\n \n {/* \ud83d\udc46 All the params must be correctly passed in */}\n {task.description}\n \n ))}\n
\n )\n}\n")),(0,r.kt)("h3",{id:"using-search-query--hash"},"Using Search Query & Hash"),(0,r.kt)("p",null,"You can also pass ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," props to the ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},'\n {task.description}\n\n')),(0,r.kt)("p",null,"This will result in a link like this: ",(0,r.kt)("inlineCode",{parentName:"p"},"/task/1?sortBy=date#comments"),". Check out the ",(0,r.kt)("a",{parentName:"p",href:"#link-component"},"API Reference")," for more details."),(0,r.kt)("h2",{id:"the-routes-object"},"The ",(0,r.kt)("inlineCode",{parentName:"h2"},"routes")," Object"),(0,r.kt)("p",null,"You can also get all the pages in your app with the ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-jsx",metastring:'title="TaskList.tsx"',title:'"TaskList.tsx"'},"import { routes } from 'wasp/client/router'\n\nconst linkToTask = routes.TaskRoute.build({ params: { id: 1 } })\n")),(0,r.kt)("p",null,"This will result in a link like this: ",(0,r.kt)("inlineCode",{parentName:"p"},"/task/1"),"."),(0,r.kt)("p",null,"You can also pass ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," props to the ",(0,r.kt)("inlineCode",{parentName:"p"},"build")," function. Check out the ",(0,r.kt)("a",{parentName:"p",href:"#routes-object"},"API Reference")," for more details."),(0,r.kt)("h2",{id:"api-reference"},"API Reference"),(0,r.kt)("h3",{id:"link-component"},(0,r.kt)("inlineCode",{parentName:"h3"},"Link")," Component"),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"Link")," component accepts the following props:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"to")," ",(0,r.kt)(i.aH,{mdxType:"Required"})),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"A valid Wasp Route path from your ",(0,r.kt)("inlineCode",{parentName:"li"},"main.wasp")," file."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"params: { [name: string]: string | number }")," ",(0,r.kt)(i.aH,{mdxType:"Required"})," (if the path contains params)"),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"An object with keys and values for each param in the path."),(0,r.kt)("li",{parentName:"ul"},"For example, if the path is ",(0,r.kt)("inlineCode",{parentName:"li"},"/task/:id"),", then the ",(0,r.kt)("inlineCode",{parentName:"li"},"params")," prop must be ",(0,r.kt)("inlineCode",{parentName:"li"},"{ id: 1 }"),". Wasp supports required and optional params."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"search: string[][] | Record | string | URLSearchParams")),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"Any valid input for ",(0,r.kt)("inlineCode",{parentName:"li"},"URLSearchParams")," constructor."),(0,r.kt)("li",{parentName:"ul"},"For example, the object ",(0,r.kt)("inlineCode",{parentName:"li"},"{ sortBy: 'date' }")," becomes ",(0,r.kt)("inlineCode",{parentName:"li"},"?sortBy=date"),"."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"p"},"hash: string"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},"all other props that the ",(0,r.kt)("inlineCode",{parentName:"p"},"react-router-dom"),"'s ",(0,r.kt)("a",{parentName:"p",href:"https://v5.reactrouter.com/web/api/Link"},"Link")," component accepts"))),(0,r.kt)("h3",{id:"routes-object"},(0,r.kt)("inlineCode",{parentName:"h3"},"routes")," Object"),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object contains a function for each route in your app."),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="router.tsx"',title:'"router.tsx"'},'export const routes = {\n // RootRoute has a path like "/"\n RootRoute: {\n build: (options?: {\n search?: string[][] | Record | string | URLSearchParams\n hash?: string\n }) => // ...\n },\n\n // DetailRoute has a path like "/task/:id/:something?"\n DetailRoute: {\n build: (\n options: {\n params: { id: ParamValue; something?: ParamValue; },\n search?: string[][] | Record | string | URLSearchParams\n hash?: string\n }\n ) => // ...\n }\n}\n')),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"params")," object is required if the route contains params. The ",(0,r.kt)("inlineCode",{parentName:"p"},"search")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"hash")," parameters are optional."),(0,r.kt)("p",null,"You can use the ",(0,r.kt)("inlineCode",{parentName:"p"},"routes")," object like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"import { routes } from 'wasp/client/router'\n\nconst linkToRoot = routes.RootRoute.build()\nconst linkToTask = routes.DetailRoute.build({ params: { id: 1 } })\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/2313c7f5.51b96fc9.js b/assets/js/0b5d4ec2.c9186086.js similarity index 80% rename from assets/js/2313c7f5.51b96fc9.js rename to assets/js/0b5d4ec2.c9186086.js index a350a224f8..406dac42d2 100644 --- a/assets/js/2313c7f5.51b96fc9.js +++ b/assets/js/0b5d4ec2.c9186086.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[85409],{85162:(e,a,t)=>{t.d(a,{Z:()=>l});var r=t(67294),n=t(86010);const o={tabItem:"tabItem_Ymn6"};function l(e){let{children:a,hidden:t,className:l}=e;return r.createElement("div",{role:"tabpanel",className:(0,n.Z)(o.tabItem,l),hidden:t},a)}},74866:(e,a,t)=>{t.d(a,{Z:()=>N});var r=t(87462),n=t(67294),o=t(86010),l=t(12466),i=t(16550),s=t(91980),d=t(67392),p=t(50012);function m(e){return function(e){return n.Children.map(e,(e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:a}=e;return!!a&&"object"==typeof a&&"value"in a}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:a,label:t,attributes:r,default:n}}=e;return{value:a,label:t,attributes:r,default:n}}))}function u(e){const{values:a,children:t}=e;return(0,n.useMemo)((()=>{const e=a??m(t);return function(e){const a=(0,d.l)(e,((e,a)=>e.value===a.value));if(a.length>0)throw new Error(`Docusaurus error: Duplicate values "${a.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[a,t])}function c(e){let{value:a,tabValues:t}=e;return t.some((e=>e.value===a))}function f(e){let{queryString:a=!1,groupId:t}=e;const r=(0,i.k6)(),o=function(e){let{queryString:a=!1,groupId:t}=e;if("string"==typeof a)return a;if(!1===a)return null;if(!0===a&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:a,groupId:t});return[(0,s._X)(o),(0,n.useCallback)((e=>{if(!o)return;const a=new URLSearchParams(r.location.search);a.set(o,e),r.replace({...r.location,search:a.toString()})}),[o,r])]}function w(e){const{defaultValue:a,queryString:t=!1,groupId:r}=e,o=u(e),[l,i]=(0,n.useState)((()=>function(e){let{defaultValue:a,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(a){if(!c({value:a,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${a}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return a}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:a,tabValues:o}))),[s,d]=f({queryString:t,groupId:r}),[m,w]=function(e){let{groupId:a}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(a),[r,o]=(0,p.Nk)(t);return[r,(0,n.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:r}),h=(()=>{const e=s??m;return c({value:e,tabValues:o})?e:null})();(0,n.useLayoutEffect)((()=>{h&&i(h)}),[h]);return{selectedValue:l,selectValue:(0,n.useCallback)((e=>{if(!c({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),d(e),w(e)}),[d,w,o]),tabValues:o}}var h=t(72389);const k={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function g(e){let{className:a,block:t,selectedValue:i,selectValue:s,tabValues:d}=e;const p=[],{blockElementScrollPositionUntilNextRender:m}=(0,l.o5)(),u=e=>{const a=e.currentTarget,t=p.indexOf(a),r=d[t].value;r!==i&&(m(a),s(r))},c=e=>{let a=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;a=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;a=p[t]??p[p.length-1];break}}a?.focus()};return n.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.Z)("tabs",{"tabs--block":t},a)},d.map((e=>{let{value:a,label:t,attributes:l}=e;return n.createElement("li",(0,r.Z)({role:"tab",tabIndex:i===a?0:-1,"aria-selected":i===a,key:a,ref:e=>p.push(e),onKeyDown:c,onClick:u},l,{className:(0,o.Z)("tabs__item",k.tabItem,l?.className,{"tabs__item--active":i===a})}),t??a)})))}function b(e){let{lazy:a,children:t,selectedValue:r}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(a){const e=o.find((e=>e.props.value===r));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return n.createElement("div",{className:"margin-top--md"},o.map(((e,a)=>(0,n.cloneElement)(e,{key:a,hidden:e.props.value!==r}))))}function v(e){const a=w(e);return n.createElement("div",{className:(0,o.Z)("tabs-container",k.tabList)},n.createElement(g,(0,r.Z)({},e,a)),n.createElement(b,(0,r.Z)({},e,a)))}function N(e){const a=(0,h.Z)();return n.createElement(v,(0,r.Z)({key:String(a)},e))}},48863:(e,a,t)=>{t.d(a,{A:()=>l,v:()=>i});var r=t(67294),n=t(50012),o=t(49875);function l(e){let{children:a}=e;const[t]=(0,n.Nk)("docusaurus.tab.js-ts");return"ts"===t&&r.createElement(o.Z,null,a)}function i(e){let{children:a}=e;const[t]=(0,n.Nk)("docusaurus.tab.js-ts");return"js"===t&&r.createElement(o.Z,null,a)}},57589:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>p,contentTitle:()=>s,default:()=>f,frontMatter:()=>i,metadata:()=>d,toc:()=>m});var r=t(87462),n=(t(67294),t(3905)),o=t(85162),l=t(74866);t(48863);const i={title:"Configuring Middleware"},s=void 0,d={unversionedId:"advanced/middleware-config",id:"version-0.12.0/advanced/middleware-config",title:"Configuring Middleware",description:"Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.",source:"@site/versioned_docs/version-0.12.0/advanced/middleware-config.md",sourceDirName:"advanced",slug:"/advanced/middleware-config",permalink:"/docs/0.12.0/advanced/middleware-config",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.12.0/advanced/middleware-config.md",tags:[],version:"0.12.0",frontMatter:{title:"Configuring Middleware"},sidebar:"docs",previous:{title:"Custom HTTP API Endpoints",permalink:"/docs/0.12.0/advanced/apis"},next:{title:"Type-Safe Links",permalink:"/docs/0.12.0/advanced/links"}},p={},m=[{value:"Default Global Middleware \ud83c\udf0d",id:"default-global-middleware-",level:2},{value:"Customization",id:"customization",level:2},{value:"Default Middleware Definitions",id:"default-middleware-definitions",level:3},{value:"1. Customize Global Middleware",id:"1-customize-global-middleware",level:2},{value:"2. Customize api-specific Middleware",id:"2-customize-api-specific-middleware",level:2},{value:"3. Customize Per-Path Middleware",id:"3-customize-per-path-middleware",level:2}],u={toc:m},c="wrapper";function f(e){let{components:a,...t}=e;return(0,n.kt)(c,(0,r.Z)({},u,t,{components:a,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-",(0,n.kt)("inlineCode",{parentName:"p"},"api"),"/path basis."),(0,n.kt)("h2",{id:"default-global-middleware-"},"Default Global Middleware \ud83c\udf0d"),(0,n.kt)("p",null,"Wasp's Express server has the following middleware by default:"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://helmetjs.github.io/"},"Helmet"),": Helmet helps you secure your Express apps by setting various HTTP headers. ",(0,n.kt)("em",{parentName:"p"},"It's not a silver bullet, but it's a good start."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/cors#readme"},"CORS"),": CORS is a package for providing a middleware that can be used to enable ",(0,n.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"},"CORS")," with various options."),(0,n.kt)("admonition",{parentName:"li",type:"note"},(0,n.kt)("p",{parentName:"admonition"},"CORS middleware is required for the frontend to communicate with the backend."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/morgan#readme"},"Morgan"),": HTTP request logger middleware.")),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/api.html#express.json"},"express.json")," (which uses ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/body-parser#bodyparserjsonoptions"},"body-parser"),"): parses incoming request bodies in a middleware before your handlers, making the result available under the ",(0,n.kt)("inlineCode",{parentName:"p"},"req.body")," property."),(0,n.kt)("admonition",{parentName:"li",type:"note"},(0,n.kt)("p",{parentName:"admonition"},"JSON middlware is required for ",(0,n.kt)("a",{parentName:"p",href:"../data-model/operations/overview"},"Operations")," to function properly."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/api.html#express.urlencoded"},"express.urlencoded")," (which uses ",(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions"},"body-parser"),"): returns middleware that only parses urlencoded bodies and only looks at requests where the ",(0,n.kt)("inlineCode",{parentName:"p"},"Content-Type")," header matches the type option.")),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/cookie-parser#readme"},"cookieParser"),": parses Cookie header and populates ",(0,n.kt)("inlineCode",{parentName:"p"},"req.cookies")," with an object keyed by the cookie names."))),(0,n.kt)("h2",{id:"customization"},"Customization"),(0,n.kt)("p",null,"You have three places where you can customize middleware:"),(0,n.kt)("ol",null,(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#1-customize-global-middleware"},"global"),": here, any changes will apply by default ",(0,n.kt)("em",{parentName:"p"},"to all operations (",(0,n.kt)("inlineCode",{parentName:"em"},"query")," and ",(0,n.kt)("inlineCode",{parentName:"em"},"action"),") and ",(0,n.kt)("inlineCode",{parentName:"em"},"api"),".")," This is helpful if you wanted to add support for multiple domains to CORS, for example."),(0,n.kt)("admonition",{parentName:"li",title:"Modifying global middleware",type:"caution"},(0,n.kt)("p",{parentName:"admonition"},"Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options."))),(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#2-customize-api-specific-middleware"},"per-api"),": you can override middleware for a specific api route (e.g. ",(0,n.kt)("inlineCode",{parentName:"p"},"POST /webhook/callback"),"). This is helpful if you want to disable JSON parsing for some callback, for example.")),(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#3-customize-per-path-middleware"},"per-path"),": this is helpful if you need to customize middleware for all methods under a given path."),(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},'It\'s helpful for things like "complex CORS requests" which may need to apply to both ',(0,n.kt)("inlineCode",{parentName:"li"},"OPTIONS")," and ",(0,n.kt)("inlineCode",{parentName:"li"},"GET"),", or to apply some middleware to a ",(0,n.kt)("em",{parentName:"li"},"set of ",(0,n.kt)("inlineCode",{parentName:"em"},"api")," routes"),".")))),(0,n.kt)("h3",{id:"default-middleware-definitions"},"Default Middleware Definitions"),(0,n.kt)("p",null,"Below is the actual definitions of default middleware which you can override."),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-js"},"const defaultGlobalMiddleware = new Map([\n ['helmet', helmet()],\n ['cors', cors({ origin: config.allowedCORSOrigins })],\n ['logger', logger('dev')],\n ['express.json', express.json()],\n ['express.urlencoded', express.urlencoded({ extended: false })],\n ['cookieParser', cookieParser()]\n])\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts"},"export type MiddlewareConfig = Map\n\n// Used in the examples below \ud83d\udc47\nexport type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig\n\nconst defaultGlobalMiddleware: MiddlewareConfig = new Map([\n ['helmet', helmet()],\n ['cors', cors({ origin: config.allowedCORSOrigins })],\n ['logger', logger('dev')],\n ['express.json', express.json()],\n ['express.urlencoded', express.urlencoded({ extended: false })],\n ['cookieParser', cookieParser()]\n])\n")))),(0,n.kt)("h2",{id:"1-customize-global-middleware"},"1. Customize Global Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for ",(0,n.kt)("em",{parentName:"p"},"all")," operations and APIs, you can do something like:"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{6} title=main.wasp","{6}":!0,title:"main.wasp"},'app todoApp {\n // ...\n\n server: {\n setupFn: import setup from "@src/serverSetup",\n middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"\n },\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/serverSetup.js",title:"src/serverSetup.js"},"import cors from 'cors'\nimport { config } from 'wasp/server'\n\nexport const serverMiddlewareFn = (middlewareConfig) => {\n // Example of adding extra domains to CORS.\n middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))\n return middlewareConfig\n}\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{6} title=main.wasp","{6}":!0,title:"main.wasp"},'app todoApp {\n // ...\n\n server: {\n setupFn: import setup from "@src/serverSetup",\n middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"\n },\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/serverSetup.ts",title:"src/serverSetup.ts"},"import cors from 'cors'\nimport { config, type MiddlewareConfigFn } from 'wasp/server'\n\nexport const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n // Example of adding an extra domains to CORS.\n middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))\n return middlewareConfig\n}\n")))),(0,n.kt)("h2",{id:"2-customize-api-specific-middleware"},"2. Customize ",(0,n.kt)("inlineCode",{parentName:"h2"},"api"),"-specific Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for a single API, you can do something like:"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{5} title=main.wasp","{5}":!0,title:"main.wasp"},'// ...\n\napi webhookCallback {\n fn: import { webhookCallback } from "@src/apis",\n middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",\n httpRoute: (POST, "/webhook/callback"),\n auth: false\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.js",title:"src/apis.js"},"import express from 'express'\n\nexport const webhookCallback = (req, res, _context) => {\n res.json({ msg: req.body.length })\n}\n\nexport const webhookCallbackMiddlewareFn = (middlewareConfig) => {\n console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')\n\n middlewareConfig.delete('express.json')\n middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))\n\n return middlewareConfig\n}\n\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{5} title=main.wasp","{5}":!0,title:"main.wasp"},'// ...\n\napi webhookCallback {\n fn: import { webhookCallback } from "@src/apis",\n middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",\n httpRoute: (POST, "/webhook/callback"),\n auth: false\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.ts",title:"src/apis.ts"},"import express from 'express'\nimport { type WebhookCallback } from 'wasp/server/api'\nimport { type MiddlewareConfigFn } from 'wasp/server'\n\nexport const webhookCallback: WebhookCallback = (req, res, _context) => {\n res.json({ msg: req.body.length })\n}\n\nexport const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')\n\n middlewareConfig.delete('express.json')\n middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))\n\n return middlewareConfig\n}\n\n")))),(0,n.kt)("admonition",{type:"note"},(0,n.kt)("p",{parentName:"admonition"},"This gets installed on a per-method basis. Behind the scenes, this results in code like:"),(0,n.kt)("pre",{parentName:"admonition"},(0,n.kt)("code",{parentName:"pre",className:"language-js"},"router.post('/webhook/callback', webhookCallbackMiddleware, ...)\n"))),(0,n.kt)("h2",{id:"3-customize-per-path-middleware"},"3. Customize Per-Path Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for all API routes under some common path, you can define a ",(0,n.kt)("inlineCode",{parentName:"p"},"middlewareConfigFn")," on an ",(0,n.kt)("inlineCode",{parentName:"p"},"apiNamespace"),":"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{4} title=main.wasp","{4}":!0,title:"main.wasp"},'// ...\n\napiNamespace fooBar {\n middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",\n path: "/foo/bar"\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.js",title:"src/apis.js"},"export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {\n const customMiddleware = (_req, _res, next) => {\n console.log('fooBarNamespaceMiddlewareFn: custom middleware')\n next()\n }\n\n middlewareConfig.set('custom.middleware', customMiddleware)\n\n return middlewareConfig\n}\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{4} title=main.wasp","{4}":!0,title:"main.wasp"},'// ...\n\napiNamespace fooBar {\n middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",\n path: "/foo/bar"\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.ts",title:"src/apis.ts"},"import express from 'express'\nimport { type MiddlewareConfigFn } from 'wasp/server'\n\nexport const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n const customMiddleware: express.RequestHandler = (_req, _res, next) => {\n console.log('fooBarNamespaceMiddlewareFn: custom middleware')\n next()\n }\n\n middlewareConfig.set('custom.middleware', customMiddleware)\n\n return middlewareConfig\n}\n")))),(0,n.kt)("admonition",{type:"note"},(0,n.kt)("p",{parentName:"admonition"},"This gets installed at the router level for the path. Behind the scenes, this results in something like:"),(0,n.kt)("pre",{parentName:"admonition"},(0,n.kt)("code",{parentName:"pre",className:"language-js"},"router.use('/foo/bar', fooBarNamespaceMiddleware)\n"))))}f.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[78423],{85162:(e,a,t)=>{t.d(a,{Z:()=>l});var r=t(67294),n=t(86010);const o={tabItem:"tabItem_Ymn6"};function l(e){let{children:a,hidden:t,className:l}=e;return r.createElement("div",{role:"tabpanel",className:(0,n.Z)(o.tabItem,l),hidden:t},a)}},74866:(e,a,t)=>{t.d(a,{Z:()=>N});var r=t(87462),n=t(67294),o=t(86010),l=t(12466),i=t(16550),s=t(91980),d=t(67392),p=t(50012);function m(e){return function(e){return n.Children.map(e,(e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:a}=e;return!!a&&"object"==typeof a&&"value"in a}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:a,label:t,attributes:r,default:n}}=e;return{value:a,label:t,attributes:r,default:n}}))}function u(e){const{values:a,children:t}=e;return(0,n.useMemo)((()=>{const e=a??m(t);return function(e){const a=(0,d.l)(e,((e,a)=>e.value===a.value));if(a.length>0)throw new Error(`Docusaurus error: Duplicate values "${a.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[a,t])}function c(e){let{value:a,tabValues:t}=e;return t.some((e=>e.value===a))}function f(e){let{queryString:a=!1,groupId:t}=e;const r=(0,i.k6)(),o=function(e){let{queryString:a=!1,groupId:t}=e;if("string"==typeof a)return a;if(!1===a)return null;if(!0===a&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:a,groupId:t});return[(0,s._X)(o),(0,n.useCallback)((e=>{if(!o)return;const a=new URLSearchParams(r.location.search);a.set(o,e),r.replace({...r.location,search:a.toString()})}),[o,r])]}function w(e){const{defaultValue:a,queryString:t=!1,groupId:r}=e,o=u(e),[l,i]=(0,n.useState)((()=>function(e){let{defaultValue:a,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(a){if(!c({value:a,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${a}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return a}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:a,tabValues:o}))),[s,d]=f({queryString:t,groupId:r}),[m,w]=function(e){let{groupId:a}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(a),[r,o]=(0,p.Nk)(t);return[r,(0,n.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:r}),h=(()=>{const e=s??m;return c({value:e,tabValues:o})?e:null})();(0,n.useLayoutEffect)((()=>{h&&i(h)}),[h]);return{selectedValue:l,selectValue:(0,n.useCallback)((e=>{if(!c({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),d(e),w(e)}),[d,w,o]),tabValues:o}}var h=t(72389);const k={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function g(e){let{className:a,block:t,selectedValue:i,selectValue:s,tabValues:d}=e;const p=[],{blockElementScrollPositionUntilNextRender:m}=(0,l.o5)(),u=e=>{const a=e.currentTarget,t=p.indexOf(a),r=d[t].value;r!==i&&(m(a),s(r))},c=e=>{let a=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const t=p.indexOf(e.currentTarget)+1;a=p[t]??p[0];break}case"ArrowLeft":{const t=p.indexOf(e.currentTarget)-1;a=p[t]??p[p.length-1];break}}a?.focus()};return n.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.Z)("tabs",{"tabs--block":t},a)},d.map((e=>{let{value:a,label:t,attributes:l}=e;return n.createElement("li",(0,r.Z)({role:"tab",tabIndex:i===a?0:-1,"aria-selected":i===a,key:a,ref:e=>p.push(e),onKeyDown:c,onClick:u},l,{className:(0,o.Z)("tabs__item",k.tabItem,l?.className,{"tabs__item--active":i===a})}),t??a)})))}function b(e){let{lazy:a,children:t,selectedValue:r}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(a){const e=o.find((e=>e.props.value===r));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return n.createElement("div",{className:"margin-top--md"},o.map(((e,a)=>(0,n.cloneElement)(e,{key:a,hidden:e.props.value!==r}))))}function v(e){const a=w(e);return n.createElement("div",{className:(0,o.Z)("tabs-container",k.tabList)},n.createElement(g,(0,r.Z)({},e,a)),n.createElement(b,(0,r.Z)({},e,a)))}function N(e){const a=(0,h.Z)();return n.createElement(v,(0,r.Z)({key:String(a)},e))}},48863:(e,a,t)=>{t.d(a,{A:()=>l,v:()=>i});var r=t(67294),n=t(50012),o=t(49875);function l(e){let{children:a}=e;const[t]=(0,n.Nk)("docusaurus.tab.js-ts");return"ts"===t&&r.createElement(o.Z,null,a)}function i(e){let{children:a}=e;const[t]=(0,n.Nk)("docusaurus.tab.js-ts");return"js"===t&&r.createElement(o.Z,null,a)}},7713:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>p,contentTitle:()=>s,default:()=>f,frontMatter:()=>i,metadata:()=>d,toc:()=>m});var r=t(87462),n=(t(67294),t(3905)),o=t(85162),l=t(74866);t(48863);const i={title:"Configuring Middleware"},s=void 0,d={unversionedId:"advanced/middleware-config",id:"version-0.13.0/advanced/middleware-config",title:"Configuring Middleware",description:"Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.",source:"@site/versioned_docs/version-0.13.0/advanced/middleware-config.md",sourceDirName:"advanced",slug:"/advanced/middleware-config",permalink:"/docs/0.13.0/advanced/middleware-config",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.13.0/advanced/middleware-config.md",tags:[],version:"0.13.0",frontMatter:{title:"Configuring Middleware"},sidebar:"docs",previous:{title:"Custom HTTP API Endpoints",permalink:"/docs/0.13.0/advanced/apis"},next:{title:"Type-Safe Links",permalink:"/docs/0.13.0/advanced/links"}},p={},m=[{value:"Default Global Middleware \ud83c\udf0d",id:"default-global-middleware-",level:2},{value:"Customization",id:"customization",level:2},{value:"Default Middleware Definitions",id:"default-middleware-definitions",level:3},{value:"1. Customize Global Middleware",id:"1-customize-global-middleware",level:2},{value:"2. Customize api-specific Middleware",id:"2-customize-api-specific-middleware",level:2},{value:"3. Customize Per-Path Middleware",id:"3-customize-per-path-middleware",level:2}],u={toc:m},c="wrapper";function f(e){let{components:a,...t}=e;return(0,n.kt)(c,(0,r.Z)({},u,t,{components:a,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-",(0,n.kt)("inlineCode",{parentName:"p"},"api"),"/path basis."),(0,n.kt)("h2",{id:"default-global-middleware-"},"Default Global Middleware \ud83c\udf0d"),(0,n.kt)("p",null,"Wasp's Express server has the following middleware by default:"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://helmetjs.github.io/"},"Helmet"),": Helmet helps you secure your Express apps by setting various HTTP headers. ",(0,n.kt)("em",{parentName:"p"},"It's not a silver bullet, but it's a good start."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/cors#readme"},"CORS"),": CORS is a package for providing a middleware that can be used to enable ",(0,n.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"},"CORS")," with various options."),(0,n.kt)("admonition",{parentName:"li",type:"note"},(0,n.kt)("p",{parentName:"admonition"},"CORS middleware is required for the frontend to communicate with the backend."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/morgan#readme"},"Morgan"),": HTTP request logger middleware.")),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/api.html#express.json"},"express.json")," (which uses ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/body-parser#bodyparserjsonoptions"},"body-parser"),"): parses incoming request bodies in a middleware before your handlers, making the result available under the ",(0,n.kt)("inlineCode",{parentName:"p"},"req.body")," property."),(0,n.kt)("admonition",{parentName:"li",type:"note"},(0,n.kt)("p",{parentName:"admonition"},"JSON middleware is required for ",(0,n.kt)("a",{parentName:"p",href:"../data-model/operations/overview"},"Operations")," to function properly."))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/api.html#express.urlencoded"},"express.urlencoded")," (which uses ",(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions"},"body-parser"),"): returns middleware that only parses urlencoded bodies and only looks at requests where the ",(0,n.kt)("inlineCode",{parentName:"p"},"Content-Type")," header matches the type option.")),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"https://github.com/expressjs/cookie-parser#readme"},"cookieParser"),": parses Cookie header and populates ",(0,n.kt)("inlineCode",{parentName:"p"},"req.cookies")," with an object keyed by the cookie names."))),(0,n.kt)("h2",{id:"customization"},"Customization"),(0,n.kt)("p",null,"You have three places where you can customize middleware:"),(0,n.kt)("ol",null,(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#1-customize-global-middleware"},"global"),": here, any changes will apply by default ",(0,n.kt)("em",{parentName:"p"},"to all operations (",(0,n.kt)("inlineCode",{parentName:"em"},"query")," and ",(0,n.kt)("inlineCode",{parentName:"em"},"action"),") and ",(0,n.kt)("inlineCode",{parentName:"em"},"api"),".")," This is helpful if you wanted to add support for multiple domains to CORS, for example."),(0,n.kt)("admonition",{parentName:"li",title:"Modifying global middleware",type:"caution"},(0,n.kt)("p",{parentName:"admonition"},"Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options."))),(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#2-customize-api-specific-middleware"},"per-api"),": you can override middleware for a specific api route (e.g. ",(0,n.kt)("inlineCode",{parentName:"p"},"POST /webhook/callback"),"). This is helpful if you want to disable JSON parsing for some callback, for example.")),(0,n.kt)("li",{parentName:"ol"},(0,n.kt)("p",{parentName:"li"},(0,n.kt)("a",{parentName:"p",href:"#3-customize-per-path-middleware"},"per-path"),": this is helpful if you need to customize middleware for all methods under a given path."),(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},'It\'s helpful for things like "complex CORS requests" which may need to apply to both ',(0,n.kt)("inlineCode",{parentName:"li"},"OPTIONS")," and ",(0,n.kt)("inlineCode",{parentName:"li"},"GET"),", or to apply some middleware to a ",(0,n.kt)("em",{parentName:"li"},"set of ",(0,n.kt)("inlineCode",{parentName:"em"},"api")," routes"),".")))),(0,n.kt)("h3",{id:"default-middleware-definitions"},"Default Middleware Definitions"),(0,n.kt)("p",null,"Below is the actual definitions of default middleware which you can override."),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-js"},"const defaultGlobalMiddleware = new Map([\n ['helmet', helmet()],\n ['cors', cors({ origin: config.allowedCORSOrigins })],\n ['logger', logger('dev')],\n ['express.json', express.json()],\n ['express.urlencoded', express.urlencoded({ extended: false })],\n ['cookieParser', cookieParser()]\n])\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts"},"export type MiddlewareConfig = Map\n\n// Used in the examples below \ud83d\udc47\nexport type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig\n\nconst defaultGlobalMiddleware: MiddlewareConfig = new Map([\n ['helmet', helmet()],\n ['cors', cors({ origin: config.allowedCORSOrigins })],\n ['logger', logger('dev')],\n ['express.json', express.json()],\n ['express.urlencoded', express.urlencoded({ extended: false })],\n ['cookieParser', cookieParser()]\n])\n")))),(0,n.kt)("h2",{id:"1-customize-global-middleware"},"1. Customize Global Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for ",(0,n.kt)("em",{parentName:"p"},"all")," operations and APIs, you can do something like:"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{6} title=main.wasp","{6}":!0,title:"main.wasp"},'app todoApp {\n // ...\n\n server: {\n setupFn: import setup from "@src/serverSetup",\n middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"\n },\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/serverSetup.js",title:"src/serverSetup.js"},"import cors from 'cors'\nimport { config } from 'wasp/server'\n\nexport const serverMiddlewareFn = (middlewareConfig) => {\n // Example of adding extra domains to CORS.\n middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))\n return middlewareConfig\n}\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{6} title=main.wasp","{6}":!0,title:"main.wasp"},'app todoApp {\n // ...\n\n server: {\n setupFn: import setup from "@src/serverSetup",\n middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"\n },\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/serverSetup.ts",title:"src/serverSetup.ts"},"import cors from 'cors'\nimport { config, type MiddlewareConfigFn } from 'wasp/server'\n\nexport const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n // Example of adding an extra domains to CORS.\n middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))\n return middlewareConfig\n}\n")))),(0,n.kt)("h2",{id:"2-customize-api-specific-middleware"},"2. Customize ",(0,n.kt)("inlineCode",{parentName:"h2"},"api"),"-specific Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for a single API, you can do something like:"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{5} title=main.wasp","{5}":!0,title:"main.wasp"},'// ...\n\napi webhookCallback {\n fn: import { webhookCallback } from "@src/apis",\n middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",\n httpRoute: (POST, "/webhook/callback"),\n auth: false\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.js",title:"src/apis.js"},"import express from 'express'\n\nexport const webhookCallback = (req, res, _context) => {\n res.json({ msg: req.body.length })\n}\n\nexport const webhookCallbackMiddlewareFn = (middlewareConfig) => {\n console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')\n\n middlewareConfig.delete('express.json')\n middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))\n\n return middlewareConfig\n}\n\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{5} title=main.wasp","{5}":!0,title:"main.wasp"},'// ...\n\napi webhookCallback {\n fn: import { webhookCallback } from "@src/apis",\n middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",\n httpRoute: (POST, "/webhook/callback"),\n auth: false\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.ts",title:"src/apis.ts"},"import express from 'express'\nimport { type WebhookCallback } from 'wasp/server/api'\nimport { type MiddlewareConfigFn } from 'wasp/server'\n\nexport const webhookCallback: WebhookCallback = (req, res, _context) => {\n res.json({ msg: req.body.length })\n}\n\nexport const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')\n\n middlewareConfig.delete('express.json')\n middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))\n\n return middlewareConfig\n}\n\n")))),(0,n.kt)("admonition",{type:"note"},(0,n.kt)("p",{parentName:"admonition"},"This gets installed on a per-method basis. Behind the scenes, this results in code like:"),(0,n.kt)("pre",{parentName:"admonition"},(0,n.kt)("code",{parentName:"pre",className:"language-js"},"router.post('/webhook/callback', webhookCallbackMiddleware, ...)\n"))),(0,n.kt)("h2",{id:"3-customize-per-path-middleware"},"3. Customize Per-Path Middleware"),(0,n.kt)("p",null,"If you would like to modify the middleware for all API routes under some common path, you can define a ",(0,n.kt)("inlineCode",{parentName:"p"},"middlewareConfigFn")," on an ",(0,n.kt)("inlineCode",{parentName:"p"},"apiNamespace"),":"),(0,n.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,n.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{4} title=main.wasp","{4}":!0,title:"main.wasp"},'// ...\n\napiNamespace fooBar {\n middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",\n path: "/foo/bar"\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.js",title:"src/apis.js"},"export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {\n const customMiddleware = (_req, _res, next) => {\n console.log('fooBarNamespaceMiddlewareFn: custom middleware')\n next()\n }\n\n middlewareConfig.set('custom.middleware', customMiddleware)\n\n return middlewareConfig\n}\n"))),(0,n.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"{4} title=main.wasp","{4}":!0,title:"main.wasp"},'// ...\n\napiNamespace fooBar {\n middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",\n path: "/foo/bar"\n}\n')),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-ts",metastring:"title=src/apis.ts",title:"src/apis.ts"},"import express from 'express'\nimport { type MiddlewareConfigFn } from 'wasp/server'\n\nexport const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {\n const customMiddleware: express.RequestHandler = (_req, _res, next) => {\n console.log('fooBarNamespaceMiddlewareFn: custom middleware')\n next()\n }\n\n middlewareConfig.set('custom.middleware', customMiddleware)\n\n return middlewareConfig\n}\n")))),(0,n.kt)("admonition",{type:"note"},(0,n.kt)("p",{parentName:"admonition"},"This gets installed at the router level for the path. Behind the scenes, this results in something like:"),(0,n.kt)("pre",{parentName:"admonition"},(0,n.kt)("code",{parentName:"pre",className:"language-js"},"router.use('/foo/bar', fooBarNamespaceMiddleware)\n"))))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0c3100e0.7e236724.js b/assets/js/0c3100e0.273b90d8.js similarity index 98% rename from assets/js/0c3100e0.7e236724.js rename to assets/js/0c3100e0.273b90d8.js index 54c90f74b6..7079aa3db0 100644 --- a/assets/js/0c3100e0.7e236724.js +++ b/assets/js/0c3100e0.273b90d8.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[35962],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>m});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function l(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),u=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},c=function(e){var t=u(e.components);return r.createElement(s.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},v=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,c=i(e,["components","mdxType","originalType","parentName"]),p=u(n),v=a,m=p["".concat(s,".").concat(v)]||p[v]||d[v]||o;return n?r.createElement(m,l(l({ref:t},c),{},{components:n})):r.createElement(m,l({ref:t},c))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,l=new Array(o);l[0]=v;var i={};for(var s in t)hasOwnProperty.call(t,s)&&(i[s]=t[s]);i.originalType=e,i[p]="string"==typeof e?e:a,l[1]=i;for(var u=2;u{n.d(t,{Z:()=>l});var r=n(67294),a=n(86010);const o={tabItem:"tabItem_Ymn6"};function l(e){let{children:t,hidden:n,className:l}=e;return r.createElement("div",{role:"tabpanel",className:(0,a.Z)(o.tabItem,l),hidden:n},t)}},74866:(e,t,n)=>{n.d(t,{Z:()=>E});var r=n(87462),a=n(67294),o=n(86010),l=n(12466),i=n(16550),s=n(91980),u=n(67392),c=n(50012);function p(e){return function(e){return a.Children.map(e,(e=>{if(!e||(0,a.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:r,default:a}}=e;return{value:t,label:n,attributes:r,default:a}}))}function d(e){const{values:t,children:n}=e;return(0,a.useMemo)((()=>{const e=t??p(n);return function(e){const t=(0,u.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function v(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:n}=e;const r=(0,i.k6)(),o=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,s._X)(o),(0,a.useCallback)((e=>{if(!o)return;const t=new URLSearchParams(r.location.search);t.set(o,e),r.replace({...r.location,search:t.toString()})}),[o,r])]}function h(e){const{defaultValue:t,queryString:n=!1,groupId:r}=e,o=d(e),[l,i]=(0,a.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!v({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const r=n.find((e=>e.default))??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:t,tabValues:o}))),[s,u]=m({queryString:n,groupId:r}),[p,h]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[r,o]=(0,c.Nk)(n);return[r,(0,a.useCallback)((e=>{n&&o.set(e)}),[n,o])]}({groupId:r}),f=(()=>{const e=s??p;return v({value:e,tabValues:o})?e:null})();(0,a.useLayoutEffect)((()=>{f&&i(f)}),[f]);return{selectedValue:l,selectValue:(0,a.useCallback)((e=>{if(!v({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),u(e),h(e)}),[u,h,o]),tabValues:o}}var f=n(72389);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function g(e){let{className:t,block:n,selectedValue:i,selectValue:s,tabValues:u}=e;const c=[],{blockElementScrollPositionUntilNextRender:p}=(0,l.o5)(),d=e=>{const t=e.currentTarget,n=c.indexOf(t),r=u[n].value;r!==i&&(p(t),s(r))},v=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=c.indexOf(e.currentTarget)+1;t=c[n]??c[0];break}case"ArrowLeft":{const n=c.indexOf(e.currentTarget)-1;t=c[n]??c[c.length-1];break}}t?.focus()};return a.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.Z)("tabs",{"tabs--block":n},t)},u.map((e=>{let{value:t,label:n,attributes:l}=e;return a.createElement("li",(0,r.Z)({role:"tab",tabIndex:i===t?0:-1,"aria-selected":i===t,key:t,ref:e=>c.push(e),onKeyDown:v,onClick:d},l,{className:(0,o.Z)("tabs__item",b.tabItem,l?.className,{"tabs__item--active":i===t})}),n??t)})))}function k(e){let{lazy:t,children:n,selectedValue:r}=e;const o=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=o.find((e=>e.props.value===r));return e?(0,a.cloneElement)(e,{className:"margin-top--md"}):null}return a.createElement("div",{className:"margin-top--md"},o.map(((e,t)=>(0,a.cloneElement)(e,{key:t,hidden:e.props.value!==r}))))}function y(e){const t=h(e);return a.createElement("div",{className:(0,o.Z)("tabs-container",b.tabList)},a.createElement(g,(0,r.Z)({},e,t)),a.createElement(k,(0,r.Z)({},e,t)))}function E(e){const t=(0,f.Z)();return a.createElement(y,(0,r.Z)({key:String(t)},e))}},46300:(e,t,n)=>{n.d(t,{Z:()=>o});var r=n(67294),a=n(50012);function o(e){let{path:t}=e;const[n]=(0,a.Nk)("docusaurus.tab.js-ts"),o=t.lastIndexOf("{"),l=t.slice(o+1,t.length-1),[i,s]=l.split(","),u=t.slice(0,o);return r.createElement("code",null,u+("js"===n?i:s))}},28117:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>m,frontMatter:()=>i,metadata:()=>u,toc:()=>p});var r=n(87462),a=(n(67294),n(3905)),o=(n(46300),n(85162)),l=n(74866);const i={title:"Env Variables"},s=void 0,u={unversionedId:"project/env-vars",id:"version-0.12.0/project/env-vars",title:"Env Variables",description:"Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.",source:"@site/versioned_docs/version-0.12.0/project/env-vars.md",sourceDirName:"project",slug:"/project/env-vars",permalink:"/docs/0.12.0/project/env-vars",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.12.0/project/env-vars.md",tags:[],version:"0.12.0",frontMatter:{title:"Env Variables"},sidebar:"docs",previous:{title:"Static Asset Handling",permalink:"/docs/0.12.0/project/static-assets"},next:{title:"Testing",permalink:"/docs/0.12.0/project/testing"}},c={},p=[{value:"Client Env Vars",id:"client-env-vars",level:2},{value:"Server Env Vars",id:"server-env-vars",level:2},{value:"Defining Env Vars in Development",id:"defining-env-vars-in-development",level:2},{value:"1. Using .env (dotenv) Files",id:"1-using-env-dotenv-files",level:3},{value:"2. Using Shell",id:"2-using-shell",level:3},{value:"Defining Env Vars in Production",id:"defining-env-vars-in-production",level:2},{value:"Client Env Vars",id:"client-env-vars-1",level:3},{value:"Server Env Vars",id:"server-env-vars-1",level:3}],d={toc:p},v="wrapper";function m(e){let{components:t,...i}=e;return(0,a.kt)(v,(0,r.Z)({},d,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Environment variables")," are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production."),(0,a.kt)("p",null,"For instance, ",(0,a.kt)("em",{parentName:"p"},"during development"),", you may want your project to connect to a local development database running on your machine, but ",(0,a.kt)("em",{parentName:"p"},"in production"),", you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account."),(0,a.kt)("p",null,"While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes."),(0,a.kt)("p",null,"In Wasp, you can use environment variables in both the client and the server code."),(0,a.kt)("h2",{id:"client-env-vars"},"Client Env Vars"),(0,a.kt)("p",null,"Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should ",(0,a.kt)("strong",{parentName:"p"},"never store secrets in them")," (such as secret API keys)."),(0,a.kt)("p",null,"To enable Wasp to pick them up, client environment variables must be prefixed with ",(0,a.kt)("inlineCode",{parentName:"p"},"REACT_APP_"),", for example: ",(0,a.kt)("inlineCode",{parentName:"p"},"REACT_APP_SOME_VAR_NAME=..."),"."),(0,a.kt)("p",null,"You can read them from the client code like this:"),(0,a.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,a.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="src/App.js"',title:'"src/App.js"'},"console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)\n"))),(0,a.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="src/App.ts"',title:'"src/App.ts"'},"console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)\n")))),(0,a.kt)("p",null,"Check below on how to define them."),(0,a.kt)("h2",{id:"server-env-vars"},"Server Env Vars"),(0,a.kt)("p",null,"In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as ",(0,a.kt)("inlineCode",{parentName:"p"},"SOME_VAR_NAME=..."),"."),(0,a.kt)("p",null,"You can read them in the server code like this:"),(0,a.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,a.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"console.log(process.env.SOME_VAR_NAME)\n"))),(0,a.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts"},"console.log(process.env.SOME_VAR_NAME)\n")))),(0,a.kt)("p",null,"Check below on how to define them."),(0,a.kt)("h2",{id:"defining-env-vars-in-development"},"Defining Env Vars in Development"),(0,a.kt)("p",null,"During development, there are two ways to provide env vars to your Wasp project:"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},"Using ",(0,a.kt)("inlineCode",{parentName:"li"},".env")," files. ",(0,a.kt)("strong",{parentName:"li"},"(recommended)")),(0,a.kt)("li",{parentName:"ol"},"Using shell. (useful for overrides)")),(0,a.kt)("h3",{id:"1-using-env-dotenv-files"},"1. Using .env (dotenv) Files"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Env vars usage in development",src:n(71203).Z,width:"908",height:"672"})),(0,a.kt)("p",null,"This is the recommended method for providing env vars to your Wasp project during development."),(0,a.kt)("p",null,"In the root of your Wasp project you can create two distinct files:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("inlineCode",{parentName:"p"},".env.server")," for env vars that will be provided to the server."),(0,a.kt)("p",{parentName:"li"},"Variables are defined in these files in the form of ",(0,a.kt)("inlineCode",{parentName:"p"},"NAME=VALUE"),", for example:"),(0,a.kt)("pre",{parentName:"li"},(0,a.kt)("code",{parentName:"pre",className:"language-shell",metastring:'title=".env.server"',title:'".env.server"'},"DATABASE_URL=postgresql://localhost:5432\nSOME_VAR_NAME=somevalue\n"))),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("inlineCode",{parentName:"p"},".env.client")," for env vars that will be provided to the client."),(0,a.kt)("p",{parentName:"li"}," Variables are defined in these files in the form of ",(0,a.kt)("inlineCode",{parentName:"p"},"NAME=VALUE"),", for example:"),(0,a.kt)("pre",{parentName:"li"},(0,a.kt)("code",{parentName:"pre",className:"language-shell",metastring:'title=".env.client"',title:'".env.client"'},"REACT_APP_SOME_VAR_NAME=somevalue\n")))),(0,a.kt)("p",null,"These files should not be committed to version control, and they are already ignored by default in the ",(0,a.kt)("inlineCode",{parentName:"p"},".gitignore")," file that comes with Wasp."),(0,a.kt)("h3",{id:"2-using-shell"},"2. Using Shell"),(0,a.kt)("p",null,"If you set environment variables in the shell where you run your Wasp commands (e.g., ",(0,a.kt)("inlineCode",{parentName:"p"},"wasp start"),"), Wasp will recognize them."),(0,a.kt)("p",null,"You can set environment variables in the ",(0,a.kt)("inlineCode",{parentName:"p"},".profile")," or a similar file, or by defining them at the start of a command:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"SOME_VAR_NAME=SOMEVALUE wasp start\n")),(0,a.kt)("p",null," This is not specific to Wasp and is simply how environment variables can be set in the shell."),(0,a.kt)("p",null,"Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally ",(0,a.kt)("strong",{parentName:"p"},"overriding")," specific environment variables because environment variables set this way ",(0,a.kt)("strong",{parentName:"p"},"take precedence over those defined in ",(0,a.kt)("inlineCode",{parentName:"strong"},".env")," files"),"."),(0,a.kt)("h2",{id:"defining-env-vars-in-production"},"Defining Env Vars in Production"),(0,a.kt)("p",null,"While in development, we had the option of using ",(0,a.kt)("inlineCode",{parentName:"p"},".env")," files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Env vars usage in development and production",src:n(68796).Z,width:"908",height:"672"})),(0,a.kt)("h3",{id:"client-env-vars-1"},"Client Env Vars"),(0,a.kt)("p",null,"Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should ",(0,a.kt)("strong",{parentName:"p"},"never store secrets in them")," (such as secret API keys)."),(0,a.kt)("p",null,"You should provide them to the build command, for example:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"REACT_APP_SOME_VAR_NAME=somevalue npm run build\n")),(0,a.kt)("admonition",{title:"How it works",type:"info"},(0,a.kt)("p",{parentName:"admonition"},"What happens behind the scenes is that Wasp will replace all occurrences of ",(0,a.kt)("inlineCode",{parentName:"p"},"import.meta.env.REACT_APP_SOME_VAR_NAME")," with the value you provided. This is done during the build process, so the value is embedded into the client code."),(0,a.kt)("p",{parentName:"admonition"},"Read more about it in Vite's ",(0,a.kt)("a",{parentName:"p",href:"https://vitejs.dev/guide/env-and-mode.html#production-replacement"},"docs"),".")),(0,a.kt)("h3",{id:"server-env-vars-1"},"Server Env Vars"),(0,a.kt)("p",null,"The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to ",(0,a.kt)("a",{parentName:"p",href:"https://fly.io"},"Fly"),", you can define them using the ",(0,a.kt)("inlineCode",{parentName:"p"},"flyctl")," CLI tool:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"flyctl secrets set SOME_VAR_NAME=somevalue\n")),(0,a.kt)("p",null,"You can read a lot more details in the ",(0,a.kt)("a",{parentName:"p",href:"../advanced/deployment/manually"},"deployment section")," of the docs. We go into detail on how to define env vars for each deployment option."))}m.isMDXComponent=!0},71203:(e,t,n)=>{n.d(t,{Z:()=>r});const r=n.p+"assets/images/prod_dev_fade-e4097e7d9b64c62ca95bfde692e5115d.svg"},68796:(e,t,n)=>{n.d(t,{Z:()=>r});const r=n.p+"assets/images/prod_dev_fade_2-d0ff1e438a29011a68bcf630a9470254.svg"}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[35962],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>m});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function l(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),u=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},c=function(e){var t=u(e.components);return r.createElement(s.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},v=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,c=i(e,["components","mdxType","originalType","parentName"]),p=u(n),v=a,m=p["".concat(s,".").concat(v)]||p[v]||d[v]||o;return n?r.createElement(m,l(l({ref:t},c),{},{components:n})):r.createElement(m,l({ref:t},c))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,l=new Array(o);l[0]=v;var i={};for(var s in t)hasOwnProperty.call(t,s)&&(i[s]=t[s]);i.originalType=e,i[p]="string"==typeof e?e:a,l[1]=i;for(var u=2;u{n.d(t,{Z:()=>l});var r=n(67294),a=n(86010);const o={tabItem:"tabItem_Ymn6"};function l(e){let{children:t,hidden:n,className:l}=e;return r.createElement("div",{role:"tabpanel",className:(0,a.Z)(o.tabItem,l),hidden:n},t)}},74866:(e,t,n)=>{n.d(t,{Z:()=>E});var r=n(87462),a=n(67294),o=n(86010),l=n(12466),i=n(16550),s=n(91980),u=n(67392),c=n(50012);function p(e){return function(e){return a.Children.map(e,(e=>{if(!e||(0,a.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:r,default:a}}=e;return{value:t,label:n,attributes:r,default:a}}))}function d(e){const{values:t,children:n}=e;return(0,a.useMemo)((()=>{const e=t??p(n);return function(e){const t=(0,u.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function v(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:n}=e;const r=(0,i.k6)(),o=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,s._X)(o),(0,a.useCallback)((e=>{if(!o)return;const t=new URLSearchParams(r.location.search);t.set(o,e),r.replace({...r.location,search:t.toString()})}),[o,r])]}function h(e){const{defaultValue:t,queryString:n=!1,groupId:r}=e,o=d(e),[l,i]=(0,a.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!v({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const r=n.find((e=>e.default))??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:t,tabValues:o}))),[s,u]=m({queryString:n,groupId:r}),[p,h]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[r,o]=(0,c.Nk)(n);return[r,(0,a.useCallback)((e=>{n&&o.set(e)}),[n,o])]}({groupId:r}),f=(()=>{const e=s??p;return v({value:e,tabValues:o})?e:null})();(0,a.useLayoutEffect)((()=>{f&&i(f)}),[f]);return{selectedValue:l,selectValue:(0,a.useCallback)((e=>{if(!v({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),u(e),h(e)}),[u,h,o]),tabValues:o}}var f=n(72389);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function g(e){let{className:t,block:n,selectedValue:i,selectValue:s,tabValues:u}=e;const c=[],{blockElementScrollPositionUntilNextRender:p}=(0,l.o5)(),d=e=>{const t=e.currentTarget,n=c.indexOf(t),r=u[n].value;r!==i&&(p(t),s(r))},v=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=c.indexOf(e.currentTarget)+1;t=c[n]??c[0];break}case"ArrowLeft":{const n=c.indexOf(e.currentTarget)-1;t=c[n]??c[c.length-1];break}}t?.focus()};return a.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.Z)("tabs",{"tabs--block":n},t)},u.map((e=>{let{value:t,label:n,attributes:l}=e;return a.createElement("li",(0,r.Z)({role:"tab",tabIndex:i===t?0:-1,"aria-selected":i===t,key:t,ref:e=>c.push(e),onKeyDown:v,onClick:d},l,{className:(0,o.Z)("tabs__item",b.tabItem,l?.className,{"tabs__item--active":i===t})}),n??t)})))}function k(e){let{lazy:t,children:n,selectedValue:r}=e;const o=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=o.find((e=>e.props.value===r));return e?(0,a.cloneElement)(e,{className:"margin-top--md"}):null}return a.createElement("div",{className:"margin-top--md"},o.map(((e,t)=>(0,a.cloneElement)(e,{key:t,hidden:e.props.value!==r}))))}function y(e){const t=h(e);return a.createElement("div",{className:(0,o.Z)("tabs-container",b.tabList)},a.createElement(g,(0,r.Z)({},e,t)),a.createElement(k,(0,r.Z)({},e,t)))}function E(e){const t=(0,f.Z)();return a.createElement(y,(0,r.Z)({key:String(t)},e))}},46300:(e,t,n)=>{n.d(t,{Z:()=>o});var r=n(67294),a=n(50012);function o(e){let{path:t}=e;const[n]=(0,a.Nk)("docusaurus.tab.js-ts"),o=t.lastIndexOf("{"),l=t.slice(o+1,t.length-1),[i,s]=l.split(","),u=t.slice(0,o);return r.createElement("code",null,u+("js"===n?i:s))}},28117:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>m,frontMatter:()=>i,metadata:()=>u,toc:()=>p});var r=n(87462),a=(n(67294),n(3905)),o=(n(46300),n(85162)),l=n(74866);const i={title:"Env Variables"},s=void 0,u={unversionedId:"project/env-vars",id:"version-0.12.0/project/env-vars",title:"Env Variables",description:"Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.",source:"@site/versioned_docs/version-0.12.0/project/env-vars.md",sourceDirName:"project",slug:"/project/env-vars",permalink:"/docs/0.12.0/project/env-vars",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/versioned_docs/version-0.12.0/project/env-vars.md",tags:[],version:"0.12.0",frontMatter:{title:"Env Variables"},sidebar:"docs",previous:{title:"Static Asset Handling",permalink:"/docs/0.12.0/project/static-assets"},next:{title:"Testing",permalink:"/docs/0.12.0/project/testing"}},c={},p=[{value:"Client Env Vars",id:"client-env-vars",level:2},{value:"Server Env Vars",id:"server-env-vars",level:2},{value:"Defining Env Vars in Development",id:"defining-env-vars-in-development",level:2},{value:"1. Using .env (dotenv) Files",id:"1-using-env-dotenv-files",level:3},{value:"2. Using Shell",id:"2-using-shell",level:3},{value:"Defining Env Vars in Production",id:"defining-env-vars-in-production",level:2},{value:"Client Env Vars",id:"client-env-vars-1",level:3},{value:"Server Env Vars",id:"server-env-vars-1",level:3}],d={toc:p},v="wrapper";function m(e){let{components:t,...i}=e;return(0,a.kt)(v,(0,r.Z)({},d,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Environment variables")," are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production."),(0,a.kt)("p",null,"For instance, ",(0,a.kt)("em",{parentName:"p"},"during development"),", you may want your project to connect to a local development database running on your machine, but ",(0,a.kt)("em",{parentName:"p"},"in production"),", you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account."),(0,a.kt)("p",null,"While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes."),(0,a.kt)("p",null,"In Wasp, you can use environment variables in both the client and the server code."),(0,a.kt)("h2",{id:"client-env-vars"},"Client Env Vars"),(0,a.kt)("p",null,"Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should ",(0,a.kt)("strong",{parentName:"p"},"never store secrets in them")," (such as secret API keys)."),(0,a.kt)("p",null,"To enable Wasp to pick them up, client environment variables must be prefixed with ",(0,a.kt)("inlineCode",{parentName:"p"},"REACT_APP_"),", for example: ",(0,a.kt)("inlineCode",{parentName:"p"},"REACT_APP_SOME_VAR_NAME=..."),"."),(0,a.kt)("p",null,"You can read them from the client code like this:"),(0,a.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,a.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="src/App.js"',title:'"src/App.js"'},"console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)\n"))),(0,a.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="src/App.ts"',title:'"src/App.ts"'},"console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)\n")))),(0,a.kt)("p",null,"Check below on how to define them."),(0,a.kt)("h2",{id:"server-env-vars"},"Server Env Vars"),(0,a.kt)("p",null,"In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as ",(0,a.kt)("inlineCode",{parentName:"p"},"SOME_VAR_NAME=..."),"."),(0,a.kt)("p",null,"You can read them in the server code like this:"),(0,a.kt)(l.Z,{groupId:"js-ts",mdxType:"Tabs"},(0,a.kt)(o.Z,{value:"js",label:"JavaScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"console.log(process.env.SOME_VAR_NAME)\n"))),(0,a.kt)(o.Z,{value:"ts",label:"TypeScript",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts"},"console.log(process.env.SOME_VAR_NAME)\n")))),(0,a.kt)("p",null,"Check below on how to define them."),(0,a.kt)("h2",{id:"defining-env-vars-in-development"},"Defining Env Vars in Development"),(0,a.kt)("p",null,"During development, there are two ways to provide env vars to your Wasp project:"),(0,a.kt)("ol",null,(0,a.kt)("li",{parentName:"ol"},"Using ",(0,a.kt)("inlineCode",{parentName:"li"},".env")," files. ",(0,a.kt)("strong",{parentName:"li"},"(recommended)")),(0,a.kt)("li",{parentName:"ol"},"Using shell. (useful for overrides)")),(0,a.kt)("h3",{id:"1-using-env-dotenv-files"},"1. Using .env (dotenv) Files"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Env vars usage in development",src:n(13641).Z,width:"908",height:"672"})),(0,a.kt)("p",null,"This is the recommended method for providing env vars to your Wasp project during development."),(0,a.kt)("p",null,"In the root of your Wasp project you can create two distinct files:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("inlineCode",{parentName:"p"},".env.server")," for env vars that will be provided to the server."),(0,a.kt)("p",{parentName:"li"},"Variables are defined in these files in the form of ",(0,a.kt)("inlineCode",{parentName:"p"},"NAME=VALUE"),", for example:"),(0,a.kt)("pre",{parentName:"li"},(0,a.kt)("code",{parentName:"pre",className:"language-shell",metastring:'title=".env.server"',title:'".env.server"'},"DATABASE_URL=postgresql://localhost:5432\nSOME_VAR_NAME=somevalue\n"))),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("p",{parentName:"li"},(0,a.kt)("inlineCode",{parentName:"p"},".env.client")," for env vars that will be provided to the client."),(0,a.kt)("p",{parentName:"li"}," Variables are defined in these files in the form of ",(0,a.kt)("inlineCode",{parentName:"p"},"NAME=VALUE"),", for example:"),(0,a.kt)("pre",{parentName:"li"},(0,a.kt)("code",{parentName:"pre",className:"language-shell",metastring:'title=".env.client"',title:'".env.client"'},"REACT_APP_SOME_VAR_NAME=somevalue\n")))),(0,a.kt)("p",null,"These files should not be committed to version control, and they are already ignored by default in the ",(0,a.kt)("inlineCode",{parentName:"p"},".gitignore")," file that comes with Wasp."),(0,a.kt)("h3",{id:"2-using-shell"},"2. Using Shell"),(0,a.kt)("p",null,"If you set environment variables in the shell where you run your Wasp commands (e.g., ",(0,a.kt)("inlineCode",{parentName:"p"},"wasp start"),"), Wasp will recognize them."),(0,a.kt)("p",null,"You can set environment variables in the ",(0,a.kt)("inlineCode",{parentName:"p"},".profile")," or a similar file, or by defining them at the start of a command:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"SOME_VAR_NAME=SOMEVALUE wasp start\n")),(0,a.kt)("p",null," This is not specific to Wasp and is simply how environment variables can be set in the shell."),(0,a.kt)("p",null,"Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally ",(0,a.kt)("strong",{parentName:"p"},"overriding")," specific environment variables because environment variables set this way ",(0,a.kt)("strong",{parentName:"p"},"take precedence over those defined in ",(0,a.kt)("inlineCode",{parentName:"strong"},".env")," files"),"."),(0,a.kt)("h2",{id:"defining-env-vars-in-production"},"Defining Env Vars in Production"),(0,a.kt)("p",null,"While in development, we had the option of using ",(0,a.kt)("inlineCode",{parentName:"p"},".env")," files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Env vars usage in development and production",src:n(52314).Z,width:"908",height:"672"})),(0,a.kt)("h3",{id:"client-env-vars-1"},"Client Env Vars"),(0,a.kt)("p",null,"Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should ",(0,a.kt)("strong",{parentName:"p"},"never store secrets in them")," (such as secret API keys)."),(0,a.kt)("p",null,"You should provide them to the build command, for example:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"REACT_APP_SOME_VAR_NAME=somevalue npm run build\n")),(0,a.kt)("admonition",{title:"How it works",type:"info"},(0,a.kt)("p",{parentName:"admonition"},"What happens behind the scenes is that Wasp will replace all occurrences of ",(0,a.kt)("inlineCode",{parentName:"p"},"import.meta.env.REACT_APP_SOME_VAR_NAME")," with the value you provided. This is done during the build process, so the value is embedded into the client code."),(0,a.kt)("p",{parentName:"admonition"},"Read more about it in Vite's ",(0,a.kt)("a",{parentName:"p",href:"https://vitejs.dev/guide/env-and-mode.html#production-replacement"},"docs"),".")),(0,a.kt)("h3",{id:"server-env-vars-1"},"Server Env Vars"),(0,a.kt)("p",null,"The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to ",(0,a.kt)("a",{parentName:"p",href:"https://fly.io"},"Fly"),", you can define them using the ",(0,a.kt)("inlineCode",{parentName:"p"},"flyctl")," CLI tool:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"flyctl secrets set SOME_VAR_NAME=somevalue\n")),(0,a.kt)("p",null,"You can read a lot more details in the ",(0,a.kt)("a",{parentName:"p",href:"../advanced/deployment/manually"},"deployment section")," of the docs. We go into detail on how to define env vars for each deployment option."))}m.isMDXComponent=!0},13641:(e,t,n)=>{n.d(t,{Z:()=>r});const r=n.p+"assets/images/prod_dev_fade-e4097e7d9b64c62ca95bfde692e5115d.svg"},52314:(e,t,n)=>{n.d(t,{Z:()=>r});const r=n.p+"assets/images/prod_dev_fade_2-d0ff1e438a29011a68bcf630a9470254.svg"}}]); \ No newline at end of file diff --git a/assets/js/0dc22d83.04d6b122.js b/assets/js/0dc22d83.04d6b122.js deleted file mode 100644 index a7d76cadd1..0000000000 --- a/assets/js/0dc22d83.04d6b122.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[56055],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>h});var a=n(67294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},p=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,i=e.originalType,l=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),u=c(n),m=s,h=u["".concat(l,".").concat(m)]||u[m]||d[m]||i;return n?a.createElement(h,r(r({ref:t},p),{},{components:n})):a.createElement(h,r({ref:t},p))}));function h(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var i=n.length,r=new Array(i);r[0]=m;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[u]="string"==typeof e?e:s,r[1]=o;for(var c=2;c{n.d(t,{Z:()=>r});var a=n(67294),s=n(39960);n(44996);const i=()=>a.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>a.createElement("p",{className:"in-blog-cta-link-container"},a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),a.createElement(i,null),a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),a.createElement(i,null),a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},36709:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>m,frontMatter:()=>r,metadata:()=>l,toc:()=>p});var a=n(87462),s=(n(67294),n(3905)),i=n(92908);const r={title:"Building an app to find an excuse for our sloppy work",authors:["maksym36ua"],tags:["wasp"]},o=void 0,l={permalink:"/blog/2022/09/05/dev-excuses-app-tutrial",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-09-05-dev-excuses-app-tutrial.md",source:"@site/blog/2022-09-05-dev-excuses-app-tutrial.md",title:"Building an app to find an excuse for our sloppy work",description:"We\u2019ll build a web app to solve every developer's most common problem \u2013 finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can\u2019t excuse ourselves from building it!",date:"2022-09-05T00:00:00.000Z",formattedDate:"September 5, 2022",tags:[{label:"wasp",permalink:"/blog/tags/wasp"}],readingTime:7.445,hasTruncateMarker:!0,authors:[{name:"Maksym Khamrovskyi",title:"DevRel @ Wasp",key:"maksym36ua"}],frontMatter:{title:"Building an app to find an excuse for our sloppy work",authors:["maksym36ua"],tags:["wasp"]},prevItem:{title:"How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)",permalink:"/blog/2022/09/29/journey-to-1000-gh-stars"},nextItem:{title:"How to get started with Haskell in 2022 (the straightforward way)",permalink:"/blog/2022/09/02/how-to-get-started-with-haskell-in-2022"}},c={authorsImageUrls:[void 0]},p=[{value:"The requirements were unclear.",id:"the-requirements-were-unclear",level:2},{value:"There\u2019s an issue with the third party library.",id:"theres-an-issue-with-the-third-party-library",level:2},{value:"Maybe something's wrong with the environment.",id:"maybe-somethings-wrong-with-the-environment",level:2},{value:"That worked perfectly when I developed it.",id:"that-worked-perfectly-when-i-developed-it",level:2},{value:"It would have taken twice as long to build it properly.",id:"it-would-have-taken-twice-as-long-to-build-it-properly",level:2}],u={toc:p},d="wrapper";function m(e){let{components:t,...r}=e;return(0,s.kt)(d,(0,a.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("p",null,"We\u2019ll build a web app to solve every developer's most common problem \u2013 finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can\u2019t excuse ourselves from building it!"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Best excuse of all time",src:n(80407).Z,width:"413",height:"360"})),(0,s.kt)("p",null,"Best excuse of all time! ",(0,s.kt)("a",{parentName:"p",href:"https://xkcd.com/303/"},"Taken from here.")),(0,s.kt)("h2",{id:"the-requirements-were-unclear"},"The requirements were unclear."),(0,s.kt)("p",null,"We\u2019ll use Michele Gerarduzzi\u2019s ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/michelegera/devexcuses-api"},"open-source project"),". It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let\u2019s define the requirements for the project: "),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"The app should be able to pull excuses data from a public API. "),(0,s.kt)("li",{parentName:"ul"},"Save the ones you liked (and your boss doesn't) to the database for future reference."),(0,s.kt)("li",{parentName:"ul"},"Building an app shouldn\u2019t take more than 15 minutes."),(0,s.kt)("li",{parentName:"ul"},"Use modern web dev technologies (NodeJS + React)")),(0,s.kt)("p",null,"As a result \u2013 we\u2019ll get a simple and fun pet project. You can find the complete codebase ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/590a08bb14284835c9785d416980da61fe9e0db0/examples/tutorials/ItWaspsOnMyMachine"},"here"),". "),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Final result",src:n(95610).Z,width:"996",height:"568"})),(0,s.kt)("h2",{id:"theres-an-issue-with-the-third-party-library"},"There\u2019s an issue with the third party library."),(0,s.kt)("p",null,"Setting up a backbone for the project is the most frustrating part of building any application. "),(0,s.kt)("p",null,"We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let\u2019s find ourselves an excuse to skip the initial project setup."),(0,s.kt)("p",null,"Ideally \u2013 use a framework that will create a project infrastructure quickly with the best defaults so that we\u2019ll focus on the business logic. A perfect candidate is ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/"},"Wasp"),". It\u2019s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate"),(0,s.kt)("p",null,"How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you\u2019ve used to have in any other full-stack app. "),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Wasp architecture",src:n(52811).Z,width:"1525",height:"696"})),(0,s.kt)("p",null,"So let\u2019s jump right in."),(0,s.kt)("h2",{id:"maybe-somethings-wrong-with-the-environment"},"Maybe something's wrong with the environment."),(0,s.kt)("p",null,"Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it\u2019s Node 16 and NPM 8. If you need another Node version for some other project \u2013 there\u2019s a possibility to ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/docs#1-requirements"},"use NVM")," to manage multiple Node versions on your computer at the same time."),(0,s.kt)("p",null,"Installing Wasp on Linux (for Mac/Windows, please ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/docs#2-installation"},"check the docs"),"):"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"curl -sSL https://get.wasp-lang.dev/installer.sh | sh\n")),(0,s.kt)("p",null,"Now let\u2019s create a new web app named ItWaspsOnMyMachine."),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"wasp new ItWaspsOnMyMachine\n")),(0,s.kt)("p",null,"Changing the working directory:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"cd ItWaspsOnMyMachine\n")),(0,s.kt)("p",null,"Starting the app:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"wasp start\n")),(0,s.kt)("p",null,"Now your default browser should open up with a simple predefined text message. That\u2019s it! \ud83e\udd73 We\u2019ve built and run a NodeJS + React application. And for now \u2013 the codebase consists of only two files! ",(0,s.kt)("inlineCode",{parentName:"p"},"main.wasp")," is the config file that defines the application\u2019s functionality. And ",(0,s.kt)("inlineCode",{parentName:"p"},"MainPage.js")," is the front-end."),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Initial page",src:n(75184).Z,width:"1891",height:"1043"})),(0,s.kt)("h2",{id:"that-worked-perfectly-when-i-developed-it"},"That worked perfectly when I developed it."),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"1) Let\u2019s add some additional configuration to our ",(0,s.kt)("inlineCode",{parentName:"strong"},"main.wasp")," file. So it will look like this:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="main.wasp | Defining Excuse entity, queries and action"',title:'"main.wasp',"|":!0,Defining:!0,Excuse:!0,"entity,":!0,queries:!0,and:!0,'action"':!0},'\n// Main declaration, defines a new web app.\napp ItWaspsOnMyMachine {\n // Wasp compiler configuration\n wasp: {\n version: "^0.6.0"\n },\n\n // Used as a browser tab title. \n title: "It Wasps On My Machine",\n\n head: [\n // Adding Tailwind to make our UI prettier\n " - - + +
-
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more →

By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more →

By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more →

By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more →

By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more →

By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

- - +
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more →

By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more →

By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more →

By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more →

By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more →

By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more →

By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

+ + \ No newline at end of file diff --git a/blog/2019/09/01/hello-wasp.html b/blog/2019/09/01/hello-wasp.html index c4a830e8df..bdff72e210 100644 --- a/blog/2019/09/01/hello-wasp.html +++ b/blog/2019/09/01/hello-wasp.html @@ -19,13 +19,13 @@ - - + +
-

Hello Wasp!

· 6 min read
Martin Sosic

About a year or so ago, brother and I started discussing how awesome it would be to have a programming language that would understand what “web app” means. Such language would, on one hand, serve as an expressive specification of the web app, while on the other hand, it would take care of “boring” work for us, while we could focus on the business logic specific for our web app.

Step by step, the idea has started to take a more concrete shape, and Wasp (Web Application SPecification language) came to life! While still very early, we are writing this blog post to explain why are we building Wasp, what is the current status and what the future may hold.

More specification, less implementation

Imagine you want to create a simple Todo web app.

You would explain it like this to your best buddy web developer: “I want to create a web app with the title ‘Todo App’ that has a single page with a list of tasks. Each task has a description and can be either marked as done or not done. The list starts as empty and tasks can be added, deleted or marked as done. I will send you designs for this. Also, I want a user to be required to register/log in.”

Now, let’s take a look at what needs to be done to implement such an app. We need to choose technologies we are going to use (frontend, backend, database, …), figure out the project file structure, set up the build toolchain, configure linting/auto-formatting/style-guide, set up tests (unit/integration, e2e), set up deployment (production, staging), set up code sharing between frontend and backend, … . Then, once everything is set up, we need to implement basic CRUD functionality (components on frontend and API on the backend), user management, probably some kind of menu on the frontend, …

We can easily see that explanation to web developer (specification) is short and concise because many details are implicit or assumed to be handled in a reasonable default way. On the other hand, implementation is complicated since it has to take care of all the details, many of them not unique for the web app we are building but common for most of the web apps. Also, if we consider the specification through time, it would look the same now and 5 years ago. On the other hand, implementation would be different, due to the new technologies that have emerged in the meantime.

So if the specification is time-resilient, short and relatively simple to describe, while implementation is complex, volatile and requires a lot of expert knowledge, how great would it be to write more of specification and less of implementation when building a web app? For that, we need more powerful languages, that will be able to express more in less code. This is where Wasp comes in.

Wasp!

The idea behind Wasp is to take everything repetitive and common in the development of a typical web app and have Wasp take care of those parts for us. Ideally, programming in Wasp would very much look like describing the specification to the web developer, therefore writing more specification and less implementation. Wasp is the one who will keep evolving and making sure your specification is implemented in the best possible technology using the industry best practices.

To achieve that, we made Wasp as a DSL (domain-specific language) that understands common concepts of a web app like pages, routes, frontend and backend and their relationship, entities, user and roles/permissions, etc. Other parts, those that are specific for our web app (business logic), we can still write in html/css/js/…, and then plug them into Wasp, combining the power of Wasp with the flexibility of existing technologies.

What’s up?

We are currently working on the first version of Wasp compiler, and are planning to soon have very first, MVP version ready. It will be just the first step of our vision of what Wasp could be, but the sooner we get it out there, the sooner we can start collecting feedback and further shaping Wasp together with the community.

We believe it will take significant effort to bring Wasp to the level where a big portion of developers will be able to build the whole app with Wasp without feeling restrained by missing flexibility or options, while on the other hand, we don’t want to wait too long until people can start using Wasp. Therefore, we decided to build it from start in such a way that a developer can at any moment “eject” from Wasp and continue on their own, where “ejecting” would mean that Wasp would generate the source code of web app that you can continue working on. That is why compiler for Wasp that we are building is actually a transpiler whose output is web app written with best practices, that you can at any moment take and continue from there if you feel too limited by Wasp. It is like having a senior developer guide you through writing a web app!

This poses the following question: “In which technologies will web app that Wasp transpiler produces be implemented?”. Well, while our vision is to offer multiple flavors here, so that you can choose the combination of technologies that you want to use, for a start we are going with one fixed technology stack, based on most popular technologies: React, Redux, NodeJS, and Mongo.

Moar

One thing that we are very excited about regarding Wasp is that Wasp understands the way web app is built. So, once you describe it in Wasp, there are many things we could be able to do with it. We could automatically generate tests since we understand the requirements. We could suggest solutions on how to improve the design of the web app. Also, since Wasp should make building web apps easier, we could build solutions on top of it, for example, a visual builder that generates Wasp code, that in turn generates a web app.

We are still very early in the Wasp journey but we are very excited about the opportunities that we imagine it could bring and about the possibilities it could unlock. We hope that this blog post will inspire others to discuss this concept and that together we will create something amazing and learn a lot on the way!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hello Wasp!

· 6 min read
Martin Sosic

About a year or so ago, brother and I started discussing how awesome it would be to have a programming language that would understand what “web app” means. Such language would, on one hand, serve as an expressive specification of the web app, while on the other hand, it would take care of “boring” work for us, while we could focus on the business logic specific for our web app.

Step by step, the idea has started to take a more concrete shape, and Wasp (Web Application SPecification language) came to life! While still very early, we are writing this blog post to explain why are we building Wasp, what is the current status and what the future may hold.

More specification, less implementation

Imagine you want to create a simple Todo web app.

You would explain it like this to your best buddy web developer: “I want to create a web app with the title ‘Todo App’ that has a single page with a list of tasks. Each task has a description and can be either marked as done or not done. The list starts as empty and tasks can be added, deleted or marked as done. I will send you designs for this. Also, I want a user to be required to register/log in.”

Now, let’s take a look at what needs to be done to implement such an app. We need to choose technologies we are going to use (frontend, backend, database, …), figure out the project file structure, set up the build toolchain, configure linting/auto-formatting/style-guide, set up tests (unit/integration, e2e), set up deployment (production, staging), set up code sharing between frontend and backend, … . Then, once everything is set up, we need to implement basic CRUD functionality (components on frontend and API on the backend), user management, probably some kind of menu on the frontend, …

We can easily see that explanation to web developer (specification) is short and concise because many details are implicit or assumed to be handled in a reasonable default way. On the other hand, implementation is complicated since it has to take care of all the details, many of them not unique for the web app we are building but common for most of the web apps. Also, if we consider the specification through time, it would look the same now and 5 years ago. On the other hand, implementation would be different, due to the new technologies that have emerged in the meantime.

So if the specification is time-resilient, short and relatively simple to describe, while implementation is complex, volatile and requires a lot of expert knowledge, how great would it be to write more of specification and less of implementation when building a web app? For that, we need more powerful languages, that will be able to express more in less code. This is where Wasp comes in.

Wasp!

The idea behind Wasp is to take everything repetitive and common in the development of a typical web app and have Wasp take care of those parts for us. Ideally, programming in Wasp would very much look like describing the specification to the web developer, therefore writing more specification and less implementation. Wasp is the one who will keep evolving and making sure your specification is implemented in the best possible technology using the industry best practices.

To achieve that, we made Wasp as a DSL (domain-specific language) that understands common concepts of a web app like pages, routes, frontend and backend and their relationship, entities, user and roles/permissions, etc. Other parts, those that are specific for our web app (business logic), we can still write in html/css/js/…, and then plug them into Wasp, combining the power of Wasp with the flexibility of existing technologies.

What’s up?

We are currently working on the first version of Wasp compiler, and are planning to soon have very first, MVP version ready. It will be just the first step of our vision of what Wasp could be, but the sooner we get it out there, the sooner we can start collecting feedback and further shaping Wasp together with the community.

We believe it will take significant effort to bring Wasp to the level where a big portion of developers will be able to build the whole app with Wasp without feeling restrained by missing flexibility or options, while on the other hand, we don’t want to wait too long until people can start using Wasp. Therefore, we decided to build it from start in such a way that a developer can at any moment “eject” from Wasp and continue on their own, where “ejecting” would mean that Wasp would generate the source code of web app that you can continue working on. That is why compiler for Wasp that we are building is actually a transpiler whose output is web app written with best practices, that you can at any moment take and continue from there if you feel too limited by Wasp. It is like having a senior developer guide you through writing a web app!

This poses the following question: “In which technologies will web app that Wasp transpiler produces be implemented?”. Well, while our vision is to offer multiple flavors here, so that you can choose the combination of technologies that you want to use, for a start we are going with one fixed technology stack, based on most popular technologies: React, Redux, NodeJS, and Mongo.

Moar

One thing that we are very excited about regarding Wasp is that Wasp understands the way web app is built. So, once you describe it in Wasp, there are many things we could be able to do with it. We could automatically generate tests since we understand the requirements. We could suggest solutions on how to improve the design of the web app. Also, since Wasp should make building web apps easier, we could build solutions on top of it, for example, a visual builder that generates Wasp code, that in turn generates a web app.

We are still very early in the Wasp journey but we are very excited about the opportunities that we imagine it could bring and about the possibilities it could unlock. We hope that this blog post will inspire others to discuss this concept and that together we will create something amazing and learn a lot on the way!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2021/02/23/journey-to-ycombinator.html b/blog/2021/02/23/journey-to-ycombinator.html index 50d8891451..bda372cbdc 100644 --- a/blog/2021/02/23/journey-to-ycombinator.html +++ b/blog/2021/02/23/journey-to-ycombinator.html @@ -19,19 +19,19 @@ - - + +
-

Journey to YCombinator

· 4 min read
Martin Sosic

Martin & Matija at YCombinator HQ

Wasp became part of Winter 2021 YCombinator batch!

Here we describe our journey and how we got in after applying for the third time.

The beginning

About 2 years ago (start of 2019) brother and I first started thinking about the idea of a (domain specific) language that is specialized for full-stack web app development - language that removes boilerplate and makes web development simpler. +

Journey to YCombinator

· 4 min read
Martin Sosic

Martin & Matija at YCombinator HQ

Wasp became part of Winter 2021 YCombinator batch!

Here we describe our journey and how we got in after applying for the third time.

The beginning

About 2 years ago (start of 2019) brother and I first started thinking about the idea of a (domain specific) language that is specialized for full-stack web app development - language that removes boilerplate and makes web development simpler. We named it Wasp (Web App SPecification).

After working on it for about a year as a side-project (researching the space, talking with potential users, building a prototype, learning), we realized it will take our full-time dedication to make something serious out of it, so we quit the current job and went all-in into Wasp, bootstrapping ourselves while working on it, to see how far we can get.

The journey to YCombinator

Due to the nature of Wasp (open-source, web framework / language), we were aware that we will need to raise funds at some point if we want to survive. We had a startup of our own previously, and we worked in multiple startups in the past, so we already knew quite a bit about how to go about it and what to expect.

Therefore, as soon as we went full-time into it (start of 2020), we immediately applied for YCombinator (top startup accelerator in the world). Soon, we got invited to the USA (we are from Europe) for the final on-site interview!

We spent weeks preparing for the interview, polishing our pitch, vision, business plan, our understanding of our users, doing mock interviews - all for those crucial 10 minutes (yes, interview lasts only 10 minutes!). At the end we didn’t pass the final interview, however we got encouraging feedback that, although we are too early, we have potential and should try applying again when we make more progress. This made a lot of sense to us, since we had only a very basic prototype and little traction.

We decided to continue working on Wasp for some longer time and continue applying to YC and talking with other interesting accelerators/investors, and see where that gets us - if nothing else, we will learn a lot on the way :)!

Half a year later, after making progress on multiple sides, we went for a second interview (this time online due to Covid) and while we felt it was really close, we still didn’t get in - they wanted to see more traction, more proof that people want it.

Finally, by the autumn of 2020, we were in a position where we had released an early-alpha version of Wasp, managed to build an initial community (>50 people on Discord, 500 Github stars) and made it to “Product of the day” on the Product Hunt. With all that we applied for the YC for the third time and made it in!

Interesting fact is that if you applied to YC previously and got rejected, that is actually a plus when you apply the next time (it show persistence, and they can see your progress). Also, while we did spend significant time preparing for the YC interviews, all that preparation also helped us get a better understanding of our idea, what our users(developers) really need and how to properly present it, so it was worth it regardless of the result of the interviews.

What now?

Right now (Feb 2020) we are in the middle of the YCombinator program, building community, talking with developers and developing Wasp toward beta.

It is still just the two of us and Wasp is in early stage, but with amazing community members on our side and with YC backing us up, we are not afraid to dream big!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/03/02/wasp-alpha.html b/blog/2021/03/02/wasp-alpha.html index 98d264a56c..37273ac3b6 100644 --- a/blog/2021/03/02/wasp-alpha.html +++ b/blog/2021/03/02/wasp-alpha.html @@ -19,12 +19,12 @@ - - + +
-

Wasp - language for developing full-stack Javascript web apps with no boilerplate

· 7 min read
Martin Sosic

Wasp logo

For the last year and a half, my twin brother and I have been working on Wasp: a new programming language for developing full-stack web apps with less code.

Wasp is a simple declarative language that makes developing web apps easy while still allowing you to use the latest technologies like React, Node.js, and Prisma.

In this post, I will share with you why we believe Wasp could be a big thing for web development, how it works, where we are right now and what is the plan for the future!

Why Wasp?

You know how to use React, know your way around HTML/CSS/…, know how to write business logic on the backend (e.g. in Node), but when you want to build an actual web app and deploy it for others to use, you drown in all the details and extra work - responsive UI, proper error handling, security, building, deployment, authentication, managing server state on the client, managing database, different environments, ....

Iceberg of web app development

Jose Aguinaga described in a fun way the unexpected complexity of web app development in his blog post "How it feels to learn JavaScript in 2016", which still feels relevant 4 years later.

We are building Wasp because even though we are both experienced developers and have worked on multiple complex web apps in various technologies (JQuery -> Backbone -> Angular -> React, own scripts / makefile -> Grunt -> Gulp -> Webpack, PHP -> Java -> Node.js, …), we still feel building web apps is harder than it should be, due to a lot of boilerplate and repetitive work involved in the process.

The main insight for us was that while the tech stack keeps advancing rapidly, the core requirements of the apps are mostly remaining the same (auth, routing, data model CRUD, ACL, …).

That is why almost 2 years ago we started thinking about separating web app specification (what it should do) from its implementation (how it should do it).
+

Wasp - language for developing full-stack Javascript web apps with no boilerplate

· 7 min read
Martin Sosic

Wasp logo

For the last year and a half, my twin brother and I have been working on Wasp: a new programming language for developing full-stack web apps with less code.

Wasp is a simple declarative language that makes developing web apps easy while still allowing you to use the latest technologies like React, Node.js, and Prisma.

In this post, I will share with you why we believe Wasp could be a big thing for web development, how it works, where we are right now and what is the plan for the future!

Why Wasp?

You know how to use React, know your way around HTML/CSS/…, know how to write business logic on the backend (e.g. in Node), but when you want to build an actual web app and deploy it for others to use, you drown in all the details and extra work - responsive UI, proper error handling, security, building, deployment, authentication, managing server state on the client, managing database, different environments, ....

Iceberg of web app development

Jose Aguinaga described in a fun way the unexpected complexity of web app development in his blog post "How it feels to learn JavaScript in 2016", which still feels relevant 4 years later.

We are building Wasp because even though we are both experienced developers and have worked on multiple complex web apps in various technologies (JQuery -> Backbone -> Angular -> React, own scripts / makefile -> Grunt -> Gulp -> Webpack, PHP -> Java -> Node.js, …), we still feel building web apps is harder than it should be, due to a lot of boilerplate and repetitive work involved in the process.

The main insight for us was that while the tech stack keeps advancing rapidly, the core requirements of the apps are mostly remaining the same (auth, routing, data model CRUD, ACL, …).

That is why almost 2 years ago we started thinking about separating web app specification (what it should do) from its implementation (how it should do it).
This led us to the idea of extracting common web app features and concepts into a special specification language (Wasp), while the implementation details are still described via a modern stack (right now React, Node.js, Prisma).

Our vision with Wasp is to create a powerful but simple language where you can describe your web app as humanly as possible. We want to make the top of that iceberg on the image above as pleasant as possible while making the bottom part much smaller.
In such language, with just a few words, you can specify pages and their routes, specify which type of authentication you want, define basic entities / data models, describe basic data flow, choose where you want to deploy, implement specific details in React/Node, and let Wasp take care of connecting it all, building it and deploying it.

Example of wasp code describing part of a simple full-stack web app.
app todoApp {
title: "ToDo App" /* visible in tab */
}

route "/" -> page Main
page Main {
component: import Main from "@ext/Main.js" /* Import your React code. */
}

auth { /* full-stack auth out-of-the-box */
userEntity: User,
methods: {
usernameAndPassword: {}
}
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

Check here for the complete example.

Why a language (DSL), aren’t frameworks solving this already?

Frameworks (like e.g. Ruby on Rails or Meteor) are a big inspiration to us. @@ -34,7 +34,7 @@ Currently, Wasp supports only Javascript, but we plan to add Typescript soon.
Technical note: Wasp compiler is implemented in Haskell.

Wasp compilation diagram

While right now only React and Node.js are supported, we plan to support multiple other technologies in the future.

Generated code is human readable and can easily be inspected and even ejected if Wasp becomes too limiting. If not ejecting, there is no need for you to ever look at the generated code - it is generated by Wasp in the background.

Wasp is used via wasp CLI - to run wasp project in development, all you need to do is run wasp start.

Wasp CLI output

Where is Wasp now and where is it going?

Our big vision is to move as much of the web app domain knowledge as possible into the Wasp language itself, giving Wasp more power and flexibility.

Ultimately, since Wasp would have such a deep understanding of the web app's requirements, we could generate a visual editor on top of it - allowing non-developers to participate in development alongside developers.

Also, Wasp wouldn't be tied to the specific technology but rather support multiple technologies (React/Angular/..., Node/Go/...**.

Wasp is currently in Alpha and some features are still rough or missing, there are things we haven’t solved yet and others that will probably change as we progress, but you can try it out and build and deploy web apps!

What Wasp currently supports:

  • ✅ full-stack auth (username & password)
  • ✅ pages & routing
  • ✅ blurs the line between client & server - define your server actions and queries and call them directly in your client code (RPC)!
  • ✅ smart caching of server actions and queries (automatic cache invalidation)
  • ✅ entity (data model) definition with Prisma.io
  • ✅ ACL on frontend
  • ✅ importing NPM dependencies

What is coming:

  • ⏳ ACL on backend
  • ⏳ one-click deployment
  • ⏳ more auth methods (Google, Linkedin, ...**
  • ⏳ tighter integration of entities with other features
  • ⏳ themes and layouts
  • ⏳ support for explicitly defined server API
  • ⏳ inline JS - the ability to mix JS code with Wasp code!
  • ⏳ Typescript support
  • ⏳ server-side rendering
  • ⏳ Visual Editor
  • ⏳ support for different languages on the backend
  • ⏳ richer wasp language with better tooling

You can check out our repo at https://github.com/wasp-lang/wasp and give it a try at https://wasp-lang.dev/docs -> we are always looking for feedback and suggestions on how to shape Wasp!

We also have a community on Discord, where we chat about Wasp-related stuff - join us to see what we are up to, share your opinions or get help with your Wasp project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/04/29/discord-bot-introduction.html b/blog/2021/04/29/discord-bot-introduction.html index ab84653bb6..5631715a11 100644 --- a/blog/2021/04/29/discord-bot-introduction.html +++ b/blog/2021/04/29/discord-bot-introduction.html @@ -19,12 +19,12 @@ - - + +
-

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

· 9 min read
Martin Sosic

Guest introducing themselves and getting full-access.
A Guest user getting access by introducing themselves in the "introductions" channel.

At Wasp, we have a Discord server for our community, where we talk with people interested in and using Wasp - Waspeteers!

In the beginning, we knew everybody in the community by their name, but as it started growing, we had a lot of people joining that never wrote anything, and the community started feeling less homey, less intimate.

This was when we decided to make it required for the new members to introduce themselves to gain access to the community. +

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

· 9 min read
Martin Sosic

Guest introducing themselves and getting full-access.
A Guest user getting access by introducing themselves in the "introductions" channel.

At Wasp, we have a Discord server for our community, where we talk with people interested in and using Wasp - Waspeteers!

In the beginning, we knew everybody in the community by their name, but as it started growing, we had a lot of people joining that never wrote anything, and the community started feeling less homey, less intimate.

This was when we decided to make it required for the new members to introduce themselves to gain access to the community. We knew that with this kind of barrier we would probably lose some potential new Waspeteers, but those that would go through it would be more engaged and better integrated.

We found no other way to accomplish this automatically but to implement our own Discord bot. In this post I will describe in detail how we did it.

High-level approach

We want the following: when a new user comes to our Discord server, they should be able to access only "public" channels, like rules, contributing, and most importantly, introductions, where they could introduce themselves.

Once they introduced themselves in the introductions channel, they would get access to the rest of the channels.

Channels user can see when Guest vs when full member.
Left: what Guest sees; Right: what Waspeteer sees.

In Discord, access control is performed via roles. There are two ways to accomplish what we need:

  1. Adding a role that grants access. When they join, they have no roles. Once they introduce themselves, they are granted a role (e.g. Member or Waspeteer) that is required to access the rest of the server.
  2. Removing a role that forbids access. When they join, they are automatically assigned a role Guest, for which we configured the non-public channels to deny access. Once they introduce themselves, the role Guest gets removed and they gain access to the rest of the server.

We decided to go with the second approach since it means we don't have to assign all the existing members with a new role. From now on, we will be talking about how to get this second approach working.

To get this going, we need to do the following:

  1. Create role Guest.
  2. Ensure that the Guest role has permissions to access only "public" channels. One convenient way to go about this is to disable "View Channels" permission for the role Guest at the level of Category, so it propagates to all the channels in it, instead of doing it for every single channel. @@ -41,7 +41,7 @@ !intro makes it easy for our bot to know when to act (in Discord, bot commands often start with !<something>).

    Let's add the needed code to bot.js:

    bot.js
    ...

    const INTRODUCTIONS_CHANNEL_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"

    bot.on('message', async msg => {
    if (msg.content.startsWith('!intro ')) {
    if (msg.channel.id.toString() !== INTRODUCTIONS_CHANNEL_ID) {
    const introductionsChannelName =
    msg.guild.channels.resolve(INTRODUCTIONS_CHANNEL_ID).name
    return msg.reply(
    `Please use !intro command in the ${introductionsChannelName} channel!`
    )
    }

    const introMsg = msg.content.substring('!intro '.length).trim()
    const minMsgLength = 20
    if (introMsg.length < minMsgLength) {
    return msg.reply(
    `Please write introduction at least ${minMsgLength} characters long!`
    )
    }

    return msg.reply(`Yay successful introduction!`)
    }
    })

    One thing to notice is that you will have to obtain the ID of the introductions channel and paste it in your code where I put the placeholder above. You can find out this ID by going to your Discord server in the Discord app, right-clicking on the introductions channel, and clicking on Copy ID. For this to work, you will first have to enable the "Developer Mode" (under "User Settings" > "Advanced").

    Removing the "Guest" role upon successful introduction

    What is missing is removing the Guest role upon successful introduction, so let's do that:

    bot.js
    ...

    const INTRODUCTIONS_CHANNEL_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"
    const GUEST_ROLE_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"

    bot.on('message', async msg => {
    if (msg.content.startsWith('!intro ')) {
    if (msg.channel.id.toString() !== INTRODUCTIONS_CHANNEL_ID) {
    const introductionsChannelName =
    msg.guild.channels.resolve(INTRODUCTIONS_CHANNEL_ID).name
    return msg.reply(
    `Please use !intro command in the ${introductionsChannelName} channel!`
    )
    }

    const introMsg = msg.content.substring('!intro '.length).trim()
    const minMsgLength = 20
    if (introMsg.length < minMsgLength) {
    return msg.reply(
    `Please write introduction at least ${minMsgLength} characters long!`
    )
    }

    const member = msg.guild.member(msg.author)
    try {
    if (member.roles.cache.get(GUEST_ROLE_ID)) {
    await member.roles.remove(GUEST_ROLE_ID)
    return msg.reply(
    'Nice getting to know you! You are no longer a guest' +
    ' and have full access, welcome!'
    )
    }
    } catch (error) {
    return msg.reply(`Error: ${error}`)
    }
    }
    })

    Same as with the ID of the introductions channel, now you will also need to find out the ID of the Guest role (which you should have created at some point). You can do it by finding it in the server settings, under the list of roles, right-clicking on it, and then "Copy ID".

    This is it! You can now run the bot with

    DISCORD_BOT=<TOKEN_OF_YOUR_DISCORD_BOT> node bot.js

    and if you assign yourself a Guest role on the Discord server and then type !intro Hi this is my introduction, I am happy to be here. in the introductions channel, you should see yourself getting full access together with an appropriate message from your bot.

    Deploying the bot

    note

    Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

    As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

    While there are many ways to deploy the Discord bot, I will shortly describe how we did it via Heroku.

    We created a Heroku app wasp-discord-bot and set up the "Automatic deploys" feature on Heroku to automatically deploy every push to the production branch (our bot is on Github).

    On Heroku, we set the environment variable DISCORD_BOT to the token of our bot.

    Finally, we added Procfile to our project:

    Procfile
    worker: node bot.js

    That is it! On every push to the production branch, our bot gets deployed.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/09/01/haskell-forall-tutorial.html b/blog/2021/09/01/haskell-forall-tutorial.html index 4c0e182a06..2aabcb994c 100644 --- a/blog/2021/09/01/haskell-forall-tutorial.html +++ b/blog/2021/09/01/haskell-forall-tutorial.html @@ -19,12 +19,12 @@ - - + +
-

Tutorial: `forall` in Haskell

· 9 min read
Martin Sosic

Find out what Haskell's forall is all about.

You might have seen forall being used in Haskell like this:

f :: forall a. [a] -> [a]
f xs = ys ++ ys
where ys :: [a]
ys = reverse xs

or

liftPair :: (forall x. x -> f x) -> (a, b) -> (f a, f b)

or

data Showable = forall s. (Show s) => Showable s

forall is something called "type quantifier", and it gives extra meaning to polymorphic type signatures (e.g. :: a, :: a -> b, :: a -> Int, ...).

While normaly forall plays a role of the "universal quantifier", it can also play a role of the "existential quantifier" (depends on the situation).

What does all this mean and how can forall be used in Haskell? Read on to find out!

NOTE: we assume you are comfortable with basic polymorphism in Haskell.

Quick math/logic reminder

In mathematical logic, we have

  • universal quantifier
    • symbol: ∀x
    • interpretation: "for all", "given any"
    • example: ∀x P(x) means "for all x predicate P(x) is true".
  • existential quantifier
    • symbol: ∃x
    • interpretation: "there exists", "there is at least one", "for some"
    • example: ∃x P(x) means "there is some x for which predicate P(x) is true".

Vanilla Haskell (no extensions)

In Haskell, all polymorphic type signatures are considered to be implicitly prefixed with forall.

Therefore, if you have

f :: a -> a
g :: a -> (a -> b) -> b

it is really the same as

f :: forall a. a -> a
g :: forall a b. a -> (a -> b) -> b

What forall here does is play the role of universal quantifier. +

Tutorial: `forall` in Haskell

· 9 min read
Martin Sosic

Find out what Haskell's forall is all about.

You might have seen forall being used in Haskell like this:

f :: forall a. [a] -> [a]
f xs = ys ++ ys
where ys :: [a]
ys = reverse xs

or

liftPair :: (forall x. x -> f x) -> (a, b) -> (f a, f b)

or

data Showable = forall s. (Show s) => Showable s

forall is something called "type quantifier", and it gives extra meaning to polymorphic type signatures (e.g. :: a, :: a -> b, :: a -> Int, ...).

While normaly forall plays a role of the "universal quantifier", it can also play a role of the "existential quantifier" (depends on the situation).

What does all this mean and how can forall be used in Haskell? Read on to find out!

NOTE: we assume you are comfortable with basic polymorphism in Haskell.

Quick math/logic reminder

In mathematical logic, we have

  • universal quantifier
    • symbol: ∀x
    • interpretation: "for all", "given any"
    • example: ∀x P(x) means "for all x predicate P(x) is true".
  • existential quantifier
    • symbol: ∃x
    • interpretation: "there exists", "there is at least one", "for some"
    • example: ∃x P(x) means "there is some x for which predicate P(x) is true".

Vanilla Haskell (no extensions)

In Haskell, all polymorphic type signatures are considered to be implicitly prefixed with forall.

Therefore, if you have

f :: a -> a
g :: a -> (a -> b) -> b

it is really the same as

f :: forall a. a -> a
g :: forall a b. a -> (a -> b) -> b

What forall here does is play the role of universal quantifier. For function f, it means it is saying "for all types, this function takes that type and returns the same type.". Other way to put it would be "this funtion can be called with value of any type as its first argument, and it will return the value of that same type".

Since forall is already implicit, writing it explicitly doesn't really do anything!

Not only that, but without any extensions, you can't even write forall explicitly, you will get a syntax error, since forall is not a keyword in Haskell.

So what is the purpose of forall then? Well, obviously to be used with extensions :)!

The simplest extension is ExplicitForAll, which allows you to explicitly write forall (as we did above). This is not useful on its own though, since as we said above, explicitly writing forall doesn't change anything, it was already implicitly there.

However, there are other extensions that make use of forall keyword, like: ScopedTypeVariables, RankNTypes, ExistentialQuantification. @@ -41,7 +41,7 @@ There would be no way to write its type signature without using RankNTypes.

forall and extension ExistentialQuantification

ExistentialQuantification enables us to use forall in the type signature of data constructors.

This is useful because it enables us to define heterogeneous data types, which then allows us to store different types in a single data collection (which normally you can't do in Haskell, e.g. you can't have different types in a list).

For example, if we have

data Showable = forall s. (Show s) => Showable s

now we can do

someShowables :: [Showable]
someShowables = [Showable "Hi", Showable 5, Showable (1, 2)]

printShowables :: [Showable] -> IO ()
printShowables ss = mapM_ (\(Showable s) -> print s) ss

main :: IO ()
main = printShowables someShowables

In this example this allowed us to create a heterogeneous list, but only thing we can do with the contents of it is show them.

What is interesting is that in this case, forall plays the role of an existential quantifier (therefore the name of extension, ExistentialQuantification), unlike the role of universal quantifier it normally plays.

GADTs

Alternative approach to ExistentialQuantification is to use the GADTs extension, like this:

{-# LANGUAGE GADTs #-}
data Showable where
Showable :: (Show s) => s -> Showable

In this case forall is not needed, as it is implicit.

forall and extension TypeApplications

TypeApplications does not change how forall works like the extensions above do, but it does have an interesting interaction with forall, so we will mention it here.

TypeApplications allows you to specify values of types variables in a type.

For example, you can do show (read @Int "5") to specify that "5" should be interpreted as an Int. read has type signature :: Read a => String -> a, so what @Int does is say that that a in the type signature is Int. Therefore, read @Int :: String -> Int.

How does forall come into play here?

Well, if an identifier’s type signature does not include an explicit forall, the type variable arguments appear in the left-to-right order in which the variables appear in the type. So, foo :: Monad m => a b -> m (a c) will have its type variables ordered as m, a, b, c, and type applications will happen in that order: if we have foo @Maybe @Either, @Maybe will apply to m while @Either will apply to a. However, if you want to force a different order, for example a, b, c, m, so that @Maybe in foo @Maybe @Either applies to a, you can refactor the signature as foo :: forall a b c m. Monad m => a b -> m (a c), and now order of type variables in forall will be used when doing type applications!

This will require you to enable ExplicitForAll extension, if it is not already enabled.

Conclusion

This document should give a fair idea of how forall is used and what can be done with it, but it doesn't go into much depth or cover all of the ways forall is used in Haskell.

For more in-detail explanations and further investigation, here is a couple of useful resources:

This blog post originated from the notes I wrote in wasp-lang/haskell-handbook.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/11/21/seed-round.html b/blog/2021/11/21/seed-round.html index 462c55b2c7..56a7721d17 100644 --- a/blog/2021/11/21/seed-round.html +++ b/blog/2021/11/21/seed-round.html @@ -19,13 +19,13 @@ - - + +
-

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

· 5 min read
Matija Sosic

After graduating from Y Combinator's Winter 2021 Batch, we are super excited to announce that Wasp raised $1.5m in our first funding round! The round is led by Lunar Ventures and joined by HV Capital. Also see it in TechCrunch.

The best thing about it is that the majority of our investors are either experienced engineers themselves (e.g. ex-Facebook, Twitter and Airbnb) or have a strong focus on investing in deep technology and developer companies. They share the vision we have with Wasp, understand and care about the problem we are solving.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Besides Lunar and HV Capital, we are thrilled to welcome on board:

  • 468 Capital (led by Florian Leibert, founder of Mesosphere and ex-Twitter and Airbnb eng.)
  • Charlie Songhurst
  • Tokyo Black
  • Acequia Capital
  • Abstraction Capital
  • Ben Tossell, founder of Makerpad (acq. by Zapier)
  • Muthukrishnan Ramabadran, Senior Software Engineer at Lyft
  • Yun-Fang, ex-Facebook engineer
  • Marcel P. Lima from Heller House
  • Chris Schagen, former CMO on Contentful
  • Rahul Thathoo, Sr. Eng. Manager at Square
  • Preetha Parthasarathy
  • John Kobs

Why did we raise funding?

At its core, Wasp is an open-source project and we have full intention for it to stay that way. Open-source is one of the most powerful ways to write software and we want to make sure Wasp is freely accessible to every developer.

Wasp is a technically innovative and challenging project. Even though we are not building a new general programming language from scratch, there still exists an essential complexity of building a language and all the tooling around it. Wasp offers a lot of abstractions that are being introduced for the first time and there is no clear blueprint to follow, and this is why such an undertaking requires full-time attention and dedication. Hence, we plan on expanding the team with some amazing engineers to accelerate us on our journey.

Where are we today?

Today, Wasp is in Alpha. That means there are many features we still have to add and many that are probably going to change. But it also means you can try it out, build a full-stack web app and see what it is all about. You can also join our community and share your feedback and experience with us - we'd be happy to hear from you!

Since we launched our Alpha several months ago, we got some amazing feedback on Product Hunt and Hacker News.

We've also grown a lot and recently passed 1,000 stars on our Github repo - thank you!

Wasp GitHub Stars

To date, over 250 projects have been created with Wasp in the last couple of months and some were even deployed to production - like Farnance that ended up being a hackathon winner! Check out their source code here.

Farnance screenshot

The team

Martin and I have been working on Wasp for the last two years and together with our amazing contributors, who made us believe our vision is possible and made it what it is today. Having led development of several complex web apps in the past and continuously switching to the latest stack, we felt the pain and could also clearly see the patterns that we felt were mature and common enough to be worth extracting into a simpler, higher-level language.

The team
Martin and I during our first YC interview. Read here for more details on our journey to YC!

In case you couldn't tell from the photo and our identical glasses, we are twins (but not fraternal ones, and I'm a couple of minutes older, which makes me CEO :D)!

We are coming from the background of C++, algorithm competitions and applied algorithms in bioinformatics (Martin built edlib, his first OSS project - a popular sequence alignment library used by top bioinfo companies like PacBio) and did our internships in Google and Palantir. There we first encountered the modern web stack and went on to lead development of web platforms in fintech and bioinformatics space. We also had a startup previously (TalkBook), where we learned a lot about talking to users and building something that solves a problem they have.

What comes next?

With the funding secured, we can now fully focus on developing Wasp and the ecosystem around it. We can start planning for more long-term features that we couldn't fully commit to until now, and we can expand our team to move faster and bring more great people on board with new perspectives and enable them to fully employ their knowledge and creativity without any distractions.

Our immediate focus is to bring Wasp to Beta and then 1.0 (see our high-level roadmap here), while also building a strong foundation for our open source community. We believe community is the key to the success for Wasp and we will do everything in our power to make sure everybody feels welcome and has a fun and rewarding experience both building apps and contributing to the project. If you want to shape how millions of engineers develop the web apps of tomorrow, join our community and work with us!

Thank you for reading - we can't wait to see what you will build!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

· 5 min read
Matija Sosic

After graduating from Y Combinator's Winter 2021 Batch, we are super excited to announce that Wasp raised $1.5m in our first funding round! The round is led by Lunar Ventures and joined by HV Capital. Also see it in TechCrunch.

The best thing about it is that the majority of our investors are either experienced engineers themselves (e.g. ex-Facebook, Twitter and Airbnb) or have a strong focus on investing in deep technology and developer companies. They share the vision we have with Wasp, understand and care about the problem we are solving.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Besides Lunar and HV Capital, we are thrilled to welcome on board:

  • 468 Capital (led by Florian Leibert, founder of Mesosphere and ex-Twitter and Airbnb eng.)
  • Charlie Songhurst
  • Tokyo Black
  • Acequia Capital
  • Abstraction Capital
  • Ben Tossell, founder of Makerpad (acq. by Zapier)
  • Muthukrishnan Ramabadran, Senior Software Engineer at Lyft
  • Yun-Fang, ex-Facebook engineer
  • Marcel P. Lima from Heller House
  • Chris Schagen, former CMO on Contentful
  • Rahul Thathoo, Sr. Eng. Manager at Square
  • Preetha Parthasarathy
  • John Kobs

Why did we raise funding?

At its core, Wasp is an open-source project and we have full intention for it to stay that way. Open-source is one of the most powerful ways to write software and we want to make sure Wasp is freely accessible to every developer.

Wasp is a technically innovative and challenging project. Even though we are not building a new general programming language from scratch, there still exists an essential complexity of building a language and all the tooling around it. Wasp offers a lot of abstractions that are being introduced for the first time and there is no clear blueprint to follow, and this is why such an undertaking requires full-time attention and dedication. Hence, we plan on expanding the team with some amazing engineers to accelerate us on our journey.

Where are we today?

Today, Wasp is in Alpha. That means there are many features we still have to add and many that are probably going to change. But it also means you can try it out, build a full-stack web app and see what it is all about. You can also join our community and share your feedback and experience with us - we'd be happy to hear from you!

Since we launched our Alpha several months ago, we got some amazing feedback on Product Hunt and Hacker News.

We've also grown a lot and recently passed 1,000 stars on our Github repo - thank you!

Wasp GitHub Stars

To date, over 250 projects have been created with Wasp in the last couple of months and some were even deployed to production - like Farnance that ended up being a hackathon winner! Check out their source code here.

Farnance screenshot

The team

Martin and I have been working on Wasp for the last two years and together with our amazing contributors, who made us believe our vision is possible and made it what it is today. Having led development of several complex web apps in the past and continuously switching to the latest stack, we felt the pain and could also clearly see the patterns that we felt were mature and common enough to be worth extracting into a simpler, higher-level language.

The team
Martin and I during our first YC interview. Read here for more details on our journey to YC!

In case you couldn't tell from the photo and our identical glasses, we are twins (but not fraternal ones, and I'm a couple of minutes older, which makes me CEO :D)!

We are coming from the background of C++, algorithm competitions and applied algorithms in bioinformatics (Martin built edlib, his first OSS project - a popular sequence alignment library used by top bioinfo companies like PacBio) and did our internships in Google and Palantir. There we first encountered the modern web stack and went on to lead development of web platforms in fintech and bioinformatics space. We also had a startup previously (TalkBook), where we learned a lot about talking to users and building something that solves a problem they have.

What comes next?

With the funding secured, we can now fully focus on developing Wasp and the ecosystem around it. We can start planning for more long-term features that we couldn't fully commit to until now, and we can expand our team to move faster and bring more great people on board with new perspectives and enable them to fully employ their knowledge and creativity without any distractions.

Our immediate focus is to bring Wasp to Beta and then 1.0 (see our high-level roadmap here), while also building a strong foundation for our open source community. We believe community is the key to the success for Wasp and we will do everything in our power to make sure everybody feels welcome and has a fun and rewarding experience both building apps and contributing to the project. If you want to shape how millions of engineers develop the web apps of tomorrow, join our community and work with us!

Thank you for reading - we can't wait to see what you will build!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2021/11/22/fundraising-learnings.html b/blog/2021/11/22/fundraising-learnings.html index 6456911cc0..47f030d2ee 100644 --- a/blog/2021/11/22/fundraising-learnings.html +++ b/blog/2021/11/22/fundraising-learnings.html @@ -19,15 +19,15 @@ - - + +
-

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

· 8 min read
Matija Sosic

Wasp fundraise chart

Wasp was part of Y Combinator’s W21 batch, which took place from January of 2021 until the end of March.

We want to share what we learned during the process!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

At Demo Day, our product had a solid traction (200+ projects created, 1k Github stars, good ProductHunt and HackerNews feedback) but no monetisation yet, which is typical for open-source projects at this stage. Being based in the EU, we also didn't have a huge network in the Bay Area prior to the fundraise.

caution

I will try to refrain from giving "general" advice (as our fundraise is a single data point), and focus on the stats and specific things that worked for us. Keep in mind the same might not work for you - I recommend always taking advice with a pinch of salt to see what makes the most sense in your case.

As we approached our fundraise, we didn't really know what to expect. We had friends from the previous batch that raised a big round very quickly (even before Demo Day) and heard a couple of stories from a few other YC founders who were also quite successful, so we imagined it might go quickly for us too.

As you can see from the title, we had quite a journey with plenty of meetings that provided us a lot of input on how to improve our pitch, and maybe even more importantly, how to reach the right investors.

Here are our stats:

  • we spoke to 212 investors → that led to 250+ meetings
  • 98 days passed between the first and the last signed SAFE
  • 171 investor passed, 24 never responded, 17 invested

And here is how it all looked when laid out on a timeline: +

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

· 8 min read
Matija Sosic

Wasp fundraise chart

Wasp was part of Y Combinator’s W21 batch, which took place from January of 2021 until the end of March.

We want to share what we learned during the process!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

At Demo Day, our product had a solid traction (200+ projects created, 1k Github stars, good ProductHunt and HackerNews feedback) but no monetisation yet, which is typical for open-source projects at this stage. Being based in the EU, we also didn't have a huge network in the Bay Area prior to the fundraise.

caution

I will try to refrain from giving "general" advice (as our fundraise is a single data point), and focus on the stats and specific things that worked for us. Keep in mind the same might not work for you - I recommend always taking advice with a pinch of salt to see what makes the most sense in your case.

As we approached our fundraise, we didn't really know what to expect. We had friends from the previous batch that raised a big round very quickly (even before Demo Day) and heard a couple of stories from a few other YC founders who were also quite successful, so we imagined it might go quickly for us too.

As you can see from the title, we had quite a journey with plenty of meetings that provided us a lot of input on how to improve our pitch, and maybe even more importantly, how to reach the right investors.

Here are our stats:

  • we spoke to 212 investors → that led to 250+ meetings
  • 98 days passed between the first and the last signed SAFE
  • 171 investor passed, 24 never responded, 17 invested

And here is how it all looked when laid out on a timeline: Wasp fundraise chart

Here are some of the things that worked for us:

We treated fundraising as a sales process (and stuck to it)

Wasp fundraise funnel

This means we had a typical sales funnel - lead generation, selling (pitching) and following up:

  • Lead generation: it started with Demo Day of course, from which we got 100+ leads but none of them ended up investing (more on that below). After that we mainly relied on our YC batchmates to identify relevant investors and get the intros.
  • Pitching: we did a conversational pitch without the deck, but we had a Notion one-pager from which I would drop links during the conversation (to e.g. our traction chart, user testimonials etc.). It also worked well as investors would typically find it interesting and keep scrolling through as we talked, asking follow-up questions.
  • Following-up: we followed up once per week. I would usually "batch process" it each Wednesday. We used Streak to identify all the leads that I haven't heard from in over 7 days (there is a filter for that) and then manually emailed them.

We started with tracking everything in Google Sheets, but with the volume of leads it soon became hard to navigate them through the funnel. Then we switched to Streak (used their fundraising template, and modified it a bit) and that worked great. The most helpful thing for me was having a CRM that is integrated with gmail, that made the process much more seamless and gave us better overview of the funnel. As soon as I would receive an email I could see in which stage the investor is, and it was also super easy to add new investors straight from gmail - it saved us from the dreaded context switching and kept us focused.

Our pitch became much better after ~50 meetings

We kept being critical of our pitch and kept a list of questions that we felt needed more work. We called it "creating narratives", e.g. why the right time for our product is now, presenting the team, or how we plan to monetise. We talked to other companies in the same space (devtools, OSS), investigated comparatives (big companies we compared ourselves too), talked to our angels who were domain experts and used all that to build a more convincing story.

I never intended to learn our pitch by heart, but after delivering it for 100s of times just that happened - both me and Martin (my brother and cofounder, who wasn't pitching but was always sitting behind me and provided feedback, especially in the beginning) knew it word by word and I realised how much more polished it sounds and how much more confident I felt compared to when we just started.

Our goal was to get to 100 no's

After about 50 meetings (and about 20 VCs having passed on us) we started feeling a bit disheartened, as things didn't seem to go so easy as we initially expected. Then I chatted to a friend who also recently finished their fundraise and he gave me a tour of Streak - I saw their numbers and that over 150 investors passed on them! With that I realised our 20 passes were just the beginning and that instead of chasing yeses we should actually chase no's :) - they are more predictable, you'll get plenty of them and they will clearly show your progress.

We had 100+ leads from Demo Day - none of them invested

This is probably pretty specific for our case, but it's how it went. Connecting with a startup on Demo Day is a very low-cost action for investors. Also, as many investors as there are on Demo Day, there are even more of them who aren't.

When we sorted through the connections we got, about 20% were a really good fit for us, meaning they invest in deep tech / OSS companies, have invested recently, invest in our stage etc.

We still met with pretty much all the interested leads, but we quickly realised that due to our product being deeply technical and the company being pre-revenue, only investors with engineering backgrounds were really interested because they could understand and get excited about what we do. That informed us to generate our leads with much narrower focus.

We looked at other OSS & dev tools companies in our batch, looked at who invested in them and asked for intros. Our batchmates were also in the fundraising mode, they knew how hard it can be and they wanted to help, so everything moved very quickly.

We learned not to spend time on non-believers

As we learned to focus on the highly qualified leads, we also learned that it is very hard (impossible) to change somebody's mind. Plenty of investors liked u and what we do, but they were skeptical about e.g. market size or monetisation potential and made that clear from the start. Many of them were keen to keep chatting, wanted to meet our angel investors etc., but none of that helped change their mind and it was very distracting for us. I believe it is very hard to change somebody's worldview, especially in the seed stage when there is often no strong factual evidence to do so.

Passing through the "valley of death"

As you can see on the chart, about two months in we barely passed $300k, and we had a whole month with no progress. At the same time, we felt that our pitch got significantly better and we were reaching investors much better suited for us. It was one of the most difficult times, seeing others close their rounds, but we decided to trust in the process and keep going until we have used all the resources we had. It was also the time our lead investor took time to do their own pretty extensive due diligence on Wasp, so although it looks like no progress was made from the outside, a lot of stuff was actually happening behind the scenes.

Suddenly, a few things clicked together from multiple sides and our round was quickly closed, even oversubscribed! It was truly a magical feeling to start closing investors in a single day, even during the first call, when previously it took us weeks to close our first $50k check. The big factor was also that our round was getting filled up and that of course motivated investors to move faster.

We compared ourselves to big, successful companies

This is one of the best pieces of advice we got from YC partners about fundraising. In the beginning we didn't understand how important this was, but once the meetings started we realised this was one of the best ways to explain the potential of our company to investors. With the innovation in technology that isn't easy to grasp, they needed something to hold on to understand how the business model and distribution could work, and it sounds much more doable if there is a playbook we can follow rather than us reinventing that as well. We kept working on finding a good comparable (we had a few) and explaining in which ways we are similar and why.

Good luck - you can do it!

I hope you found this helpful and that our story will motivate you to keep going once things get hard! We wish you the best of luck and also feel free to reach out if you'll have any questions.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/12/02/waspello.html b/blog/2021/12/02/waspello.html index 9d8e26da61..9f10d8be66 100644 --- a/blog/2021/12/02/waspello.html +++ b/blog/2021/12/02/waspello.html @@ -19,15 +19,15 @@ - - + +
-

How we built a Trello clone with Wasp - Waspello!

· 10 min read
Matija Sosic

Enter Waspello

Try Waspello here! | See the code

We've built a Trello clone using Wasp! Read on to learn how it went and how you can contribute.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Why Trello?

While building Wasp, our goal is to use it as much as we can to build our projects and play with it, so we can learn what works and what we should do next. This is why Trello was a great choice of app to build with Wasp - it is one of the most well-known full-stack web apps, it's very simple and intuitive to use but also covers a good portion of features used by today's modern web apps.

So let's dig in and see and how it went - what works, what doesn't and, what's missing/coming next!

What works?

It's alive ⚡🤖 !!

The good news is all the basic functionality is here - Waspello users can signup/log in which brings them to their project board where they can perform CRUD operations on lists and cards - create them, edit them, move them around, etc. Let's see it in action:

Waspello in action

Waspello in action!

As you can see things work, but not everything is perfect (e.g. there is a delay when creating/moving a card) - we'll examine why is that so a bit later.

Under the hood 🚘 🔧

Here is a simple visual overview of Waspello's code anatomy (which applies to every Wasp app):

Waspello code anatomy

Waspello code anatomy

Let's now dig in a bit deeper and shortly examine each of the concepts Wasp supports (page, query, entity, ...) and learn through code samples how to use it to implement Waspello.

Entities

It all starts with a data model definition (called entity in Wasp), which is defined via Prisma Schema Language:

main.wasp | Defining entities via Prisma Schema Language
// Entities

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
lists List[]
cards Card[]
psl=}

entity List {=psl
id Int @id @default(autoincrement())
name String
pos Float

// List has a single author.
user User @relation(fields: [userId], references: [id])
userId Int

cards Card[]
psl=}

entity Card {=psl
id Int @id @default(autoincrement())
title String
pos Float

// Card belongs to a single list.
list List @relation(fields: [listId], references: [id])
listId Int

// Card has a single author.
author User @relation(fields: [authorId], references: [id])
authorId Int
psl=}

Those three entities are all we need! Wasp uses Prisma to create a database schema underneath and allows the developer to query it through its generated SDK.

Queries and Actions (Operations)

After we've defined our data models, the next step is to do something with them! We can read/create/update/delete an entity and that is what query and action mechanisms are for. Below follows an example from the Waspello code that demonstrates how it works.

The first step is to declare to Wasp there will be a query, point to the actual function containing the query logic, and state from which entities it will be reading information:

main.wasp | Declaration of a query in Wasp
query getListsAndCards {
// Points to the function which contains query logic.
fn: import { getListsAndCards } from "@server/queries.js",

// This query depends on List and Card entities.
// If any of them changes this query will get re-fetched (cache invalidation).
entities: [List, Card]
}

The main point of this declaration is for Wasp to be aware of the query and thus be able to do a lot of heavy lifting for us - e.g. it will make the query available to the client without any extra code, all that developer needs to do is import it in their React component. Another big thing is cache invalidation / automatic re-fetching of the query once the data changes (this is why it is important to declare which entities it depends on).

The remaining step is to write the function with the query logic:

src/server/queries.js | Query logic, using Prisma SDK via Node.js
export const getListsAndCards = async (args, context) => {
// Only authenticated users can execute this query.
if (!context.user) { throw new HttpError(403) }

return context.entities.List.findMany({
// We want to make sure user can access only their own cards.
where: { user: { id: context.user.id } },
include: { cards: true }
})
}

This is just a regular Node.js function, there are no limits on what you can return! All the stuff provided by Wasp (user data, Prisma SDK for a specific entity) comes in a context variable.

The code for actions is very similar (we just need to use action keyword instead of query) so I won't repeat it here. You can check out the code for updateCard action here.

Pages, routing & components

To display all the nice data we have, we'll use React components. There are no limits to how you can use React components within Wasp, the only one is that each page has its root component:

main.wasp | Declaration of a page & route in Wasp
route MainRoute { path: "/", to: Main }
page Main {
authRequired: true,
component: import Main from "@client/MainPage.js"
}

All pretty straightforward so far! As you can see here, Wasp also provides authentication out-of-the-box.

Currently, the majority of the client logic of Waspello is contained in src/client/MainPage.js (we should break it down a little 😅 - you can help us!). Just to give you an idea, here's a quick glimpse into it:

src/client/MainPage.js | Using React component in Wasp
// "Special" imports provided by Wasp.
import { useQuery } from '@wasp/queries'
import getListsAndCards from '@wasp/queries/getListsAndCards'
import createList from '@wasp/actions/createList'

const MainPage = ({ user }) => {
// Fetching data via useQuery.
const { data: listsAndCards, isFetchingListsAndCards, errorListsAndCards }
= useQuery(getListsAndCards)

// A lot of data transformations and sub components.
...

// Display lists and cards.
return (
...
)
}

Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the @wasp prefix in the import path. useQuery ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it here.

This is pretty much it from the stuff that works 😄 ! I kinda rushed a bit through things here - for more details on all Wasp features and to build your first app with Wasp, check out our docs.

What doesn't work (yet)

The main problem of the current implementation of Waspello is the lack of support for optimistic UI updates in Wasp. What this means is that currently, when an entity-related change is made (e.g. a card is moved from one list to another), we have to wait until that change is fully executed on the server until it is visible in the UI, which causes a noticeable delay.
+

How we built a Trello clone with Wasp - Waspello!

· 10 min read
Matija Sosic

Enter Waspello

Try Waspello here! | See the code

We've built a Trello clone using Wasp! Read on to learn how it went and how you can contribute.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Why Trello?

While building Wasp, our goal is to use it as much as we can to build our projects and play with it, so we can learn what works and what we should do next. This is why Trello was a great choice of app to build with Wasp - it is one of the most well-known full-stack web apps, it's very simple and intuitive to use but also covers a good portion of features used by today's modern web apps.

So let's dig in and see and how it went - what works, what doesn't and, what's missing/coming next!

What works?

It's alive ⚡🤖 !!

The good news is all the basic functionality is here - Waspello users can signup/log in which brings them to their project board where they can perform CRUD operations on lists and cards - create them, edit them, move them around, etc. Let's see it in action:

Waspello in action

Waspello in action!

As you can see things work, but not everything is perfect (e.g. there is a delay when creating/moving a card) - we'll examine why is that so a bit later.

Under the hood 🚘 🔧

Here is a simple visual overview of Waspello's code anatomy (which applies to every Wasp app):

Waspello code anatomy

Waspello code anatomy

Let's now dig in a bit deeper and shortly examine each of the concepts Wasp supports (page, query, entity, ...) and learn through code samples how to use it to implement Waspello.

Entities

It all starts with a data model definition (called entity in Wasp), which is defined via Prisma Schema Language:

main.wasp | Defining entities via Prisma Schema Language
// Entities

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
lists List[]
cards Card[]
psl=}

entity List {=psl
id Int @id @default(autoincrement())
name String
pos Float

// List has a single author.
user User @relation(fields: [userId], references: [id])
userId Int

cards Card[]
psl=}

entity Card {=psl
id Int @id @default(autoincrement())
title String
pos Float

// Card belongs to a single list.
list List @relation(fields: [listId], references: [id])
listId Int

// Card has a single author.
author User @relation(fields: [authorId], references: [id])
authorId Int
psl=}

Those three entities are all we need! Wasp uses Prisma to create a database schema underneath and allows the developer to query it through its generated SDK.

Queries and Actions (Operations)

After we've defined our data models, the next step is to do something with them! We can read/create/update/delete an entity and that is what query and action mechanisms are for. Below follows an example from the Waspello code that demonstrates how it works.

The first step is to declare to Wasp there will be a query, point to the actual function containing the query logic, and state from which entities it will be reading information:

main.wasp | Declaration of a query in Wasp
query getListsAndCards {
// Points to the function which contains query logic.
fn: import { getListsAndCards } from "@server/queries.js",

// This query depends on List and Card entities.
// If any of them changes this query will get re-fetched (cache invalidation).
entities: [List, Card]
}

The main point of this declaration is for Wasp to be aware of the query and thus be able to do a lot of heavy lifting for us - e.g. it will make the query available to the client without any extra code, all that developer needs to do is import it in their React component. Another big thing is cache invalidation / automatic re-fetching of the query once the data changes (this is why it is important to declare which entities it depends on).

The remaining step is to write the function with the query logic:

src/server/queries.js | Query logic, using Prisma SDK via Node.js
export const getListsAndCards = async (args, context) => {
// Only authenticated users can execute this query.
if (!context.user) { throw new HttpError(403) }

return context.entities.List.findMany({
// We want to make sure user can access only their own cards.
where: { user: { id: context.user.id } },
include: { cards: true }
})
}

This is just a regular Node.js function, there are no limits on what you can return! All the stuff provided by Wasp (user data, Prisma SDK for a specific entity) comes in a context variable.

The code for actions is very similar (we just need to use action keyword instead of query) so I won't repeat it here. You can check out the code for updateCard action here.

Pages, routing & components

To display all the nice data we have, we'll use React components. There are no limits to how you can use React components within Wasp, the only one is that each page has its root component:

main.wasp | Declaration of a page & route in Wasp
route MainRoute { path: "/", to: Main }
page Main {
authRequired: true,
component: import Main from "@client/MainPage.js"
}

All pretty straightforward so far! As you can see here, Wasp also provides authentication out-of-the-box.

Currently, the majority of the client logic of Waspello is contained in src/client/MainPage.js (we should break it down a little 😅 - you can help us!). Just to give you an idea, here's a quick glimpse into it:

src/client/MainPage.js | Using React component in Wasp
// "Special" imports provided by Wasp.
import { useQuery } from '@wasp/queries'
import getListsAndCards from '@wasp/queries/getListsAndCards'
import createList from '@wasp/actions/createList'

const MainPage = ({ user }) => {
// Fetching data via useQuery.
const { data: listsAndCards, isFetchingListsAndCards, errorListsAndCards }
= useQuery(getListsAndCards)

// A lot of data transformations and sub components.
...

// Display lists and cards.
return (
...
)
}

Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the @wasp prefix in the import path. useQuery ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it here.

This is pretty much it from the stuff that works 😄 ! I kinda rushed a bit through things here - for more details on all Wasp features and to build your first app with Wasp, check out our docs.

What doesn't work (yet)

The main problem of the current implementation of Waspello is the lack of support for optimistic UI updates in Wasp. What this means is that currently, when an entity-related change is made (e.g. a card is moved from one list to another), we have to wait until that change is fully executed on the server until it is visible in the UI, which causes a noticeable delay.
In many cases that is not an issue, but when UI elements are all visible at once and it is expected from them to be updated immediately, then it is noticeable. This is also one of the main reasons why we chose to work on Waspello - to have a benchmark/sandbox for this feature! Due to this issue, here's how things currently look like:

Waspello - no optimistic UI update
Without an optimistic UI update, there is a delay

You can notice the delay between the moment the card is dropped on the "Done" list and the moment it becomes a part of that list. The reason is that at the moment of dropping the card on "Done" list, the API request with the change is sent to the server, and only when that change is fully processed on the server and saved to the database, the query getListsAndCards returns the correct info and consequently, UI is updated to the correct state.
That is why upon dropping on "Done", the card first goes back to the original list (because the change is not saved in db yet, so useQuery(getListsAndCards) still returns the "old" state), it waits a bit until the API request is processed successfully, and just then the change gets reflected in the UI.

The solution

A typical approach for solving this issue is to make the client a bit more self-confident, in a way that it doesn't wait for the confirmation from the server but rather immediately updates the UI, at the same time or even before the API request is fired. If it then turns out something went wrong on the server (which typically shouldn't happen), it reverses the change and shows an error message. Thus the name optimistic UI update, since the client assumes in advance that everything will go well to provide a nicer UX.

Waspello - the client being brave
The client when performing an optimistic UI update

This is one of the most complex and error-prone features when developing web apps today and that is why we are super excited to tackle it in Wasp and make the experience as smooth as possible! We are currently in the "figuring out the solution" stage and you can track/join the discussion on GitHub!

What's missing (next features)

Although it looks super simple at the first glance, Trello is in fact a huge app with lots and lots of cool features hidden under the surface! Here are some of the more obvious ones that are currently not supported in Waspello:

  • Users can have multiple boards, for different projects (currently we have no notion of a "Board" entity in Waspello at all, so there is implicitly only one)
  • Detailed card view - when clicked on a card, a "full" view with extra options opens
  • Search - user can search for a specific list/card
  • Collaboration - multiple users can participate on the same board

And many more - e.g. support for workspaces (next level of the hierarchy, a collection of boards), card labels, filters, ... . It is very helpful to have such a variety of features since we can use it as a testing ground for Wasp and use it as a guiding star towards Beta/1.0!

Become a Waspeller!

Waspello propaganda
Lightweight Waspello propaganda

If you want to get involved with OSS and at the same time familiarize yourself with Wasp, this is a great way to get started - feel free to choose one of the features listed here or add your own and help us make Waspello the best demo productivity app out there!

Also, make sure to join our community on Discord. We’re always there and are looking forward to seeing what you build!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/12/21/shayne-intro.html b/blog/2021/12/21/shayne-intro.html index 1797412be0..a18cad7683 100644 --- a/blog/2021/12/21/shayne-intro.html +++ b/blog/2021/12/21/shayne-intro.html @@ -19,13 +19,13 @@ - - + +
-

Meet the team - Shayne Czyzewski, Founding Engineer

· 4 min read
Matija Sosic

Welcome Shayne!

Find Shayne on Twitter and GitHub.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

We are super excited to introduce Shayne, the first person to join the Wasp team! Shayne is a battle-tested veteran engineer, with experiences ranging from leading teams at high-growth startups to working at enterprise giants such as Red Hat and NetApp. Along with that, he is super nice and incredibly pleasant to work with - we are beyond thrilled that he chose Wasp for his next adventure with him and can't wait for you to meet him in our Discord community!

Why did you join Wasp?

I have always been excited about high-quality dev tooling and web frameworks, and I am also interested in Haskell/compilers. The technology, problem space, and team were just too compelling to pass up. I was also excited to be on the ground floor of a YC startup, where I can have a significant impact and help build a broad, welcoming, open-source community of Wasp developers.

What did you do before?

I have been a professional developer for over a decade, mostly in backend web development, with experience from Lockheed Martin, Morgan Stanley, NetApp, and Red Hat. Most recently, I was the head of engineering at an edtech company called LearnPlatform, where we were handling a quarter of a billion incoming events per day with the goal of understanding and improving student access to technology that works best for them.

What is your favorite language/framework?

My favorite framework is probably Ruby on Rails, for the elegance of ideas and seamless implementation. I never had an actual favorite programming language, as I enjoy different aspects of Ruby, Elixir, JavaScript, C#, and others. My least favorite has always been Java. My current favorite language is fast becoming Haskell. :)

The most interesting niche programming language I have used professionally was Ada at Lockheed Martin. We used it to build distributed, real-time, full-motion flight simulators for the military (think multi-million dollar, hyperrealistic multiplayer video games).

What are you most excited about in Wasp?

As web developers, I think we have gotten accustomed to a certain level of complexity that is not associated with the problem we are solving but the boilerplate of the process. This lack of nuance between accidental and essential complexity has recently led to less than ideal low-code approaches. Wasp, in my view, takes the better approach of a higher-level DSL to abstract some of the typical details using best practices, leaving you to focus on your problem by writing actual code that produces a real web app without any vendor lock-in. That is pretty amazing to me!

How did you start coding?

Probably by creating some basic LAMP apps in the late 90s while in high school. Growing up, our parents wanted us to have summer jobs to earn money we could spend during the rest of the year. I quickly found that freelance web development on Elance, and similar sites, was more enjoyable and profitable than the alternatives available to 15-year-olds. From then on, I was hooked.

What is your dev setup?

MacBook Air M1 with an external Dell display, Magic Trackpad, and a split mechanical keyboard from UHK (Ultimate Hacking Keyboard).

camelCase or snake_case?

I default to whatever the language or codebase conventions are. Visually, I prefer snake case, though (and definitely spaces over tabs). ;)

What's one piece of advice you'd give to an aspiring developer?

One of the biggest differentiators I have found between good and great engineers is that the great ones possess a continuous desire to learn and grow. They view challenges as fun opportunities to expand their knowledge and skills, recognizing that they always have room for improvement. The corollary is that impostor syndrome is real and never goes away, so try not to be too hard on yourself along the way!

This post was the first of several new hire announcements in the months to come, so stay tuned and reach out if you want to work with Martin, Shayne, and myself!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Meet the team - Shayne Czyzewski, Founding Engineer

· 4 min read
Matija Sosic

Welcome Shayne!

Find Shayne on Twitter and GitHub.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

We are super excited to introduce Shayne, the first person to join the Wasp team! Shayne is a battle-tested veteran engineer, with experiences ranging from leading teams at high-growth startups to working at enterprise giants such as Red Hat and NetApp. Along with that, he is super nice and incredibly pleasant to work with - we are beyond thrilled that he chose Wasp for his next adventure with him and can't wait for you to meet him in our Discord community!

Why did you join Wasp?

I have always been excited about high-quality dev tooling and web frameworks, and I am also interested in Haskell/compilers. The technology, problem space, and team were just too compelling to pass up. I was also excited to be on the ground floor of a YC startup, where I can have a significant impact and help build a broad, welcoming, open-source community of Wasp developers.

What did you do before?

I have been a professional developer for over a decade, mostly in backend web development, with experience from Lockheed Martin, Morgan Stanley, NetApp, and Red Hat. Most recently, I was the head of engineering at an edtech company called LearnPlatform, where we were handling a quarter of a billion incoming events per day with the goal of understanding and improving student access to technology that works best for them.

What is your favorite language/framework?

My favorite framework is probably Ruby on Rails, for the elegance of ideas and seamless implementation. I never had an actual favorite programming language, as I enjoy different aspects of Ruby, Elixir, JavaScript, C#, and others. My least favorite has always been Java. My current favorite language is fast becoming Haskell. :)

The most interesting niche programming language I have used professionally was Ada at Lockheed Martin. We used it to build distributed, real-time, full-motion flight simulators for the military (think multi-million dollar, hyperrealistic multiplayer video games).

What are you most excited about in Wasp?

As web developers, I think we have gotten accustomed to a certain level of complexity that is not associated with the problem we are solving but the boilerplate of the process. This lack of nuance between accidental and essential complexity has recently led to less than ideal low-code approaches. Wasp, in my view, takes the better approach of a higher-level DSL to abstract some of the typical details using best practices, leaving you to focus on your problem by writing actual code that produces a real web app without any vendor lock-in. That is pretty amazing to me!

How did you start coding?

Probably by creating some basic LAMP apps in the late 90s while in high school. Growing up, our parents wanted us to have summer jobs to earn money we could spend during the rest of the year. I quickly found that freelance web development on Elance, and similar sites, was more enjoyable and profitable than the alternatives available to 15-year-olds. From then on, I was hooked.

What is your dev setup?

MacBook Air M1 with an external Dell display, Magic Trackpad, and a split mechanical keyboard from UHK (Ultimate Hacking Keyboard).

camelCase or snake_case?

I default to whatever the language or codebase conventions are. Visually, I prefer snake case, though (and definitely spaces over tabs). ;)

What's one piece of advice you'd give to an aspiring developer?

One of the biggest differentiators I have found between good and great engineers is that the great ones possess a continuous desire to learn and grow. They view challenges as fun opportunities to expand their knowledge and skills, recognizing that they always have room for improvement. The corollary is that impostor syndrome is real and never goes away, so try not to be too hard on yourself along the way!

This post was the first of several new hire announcements in the months to come, so stay tuned and reach out if you want to work with Martin, Shayne, and myself!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/01/27/waspleau.html b/blog/2022/01/27/waspleau.html index 66336c8c03..894ad49e89 100644 --- a/blog/2022/01/27/waspleau.html +++ b/blog/2022/01/27/waspleau.html @@ -19,13 +19,13 @@ - - + +
-

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

· 5 min read
Shayne Czyzewski

Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau-app-client.fly.dev/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

· 5 min read
Shayne Czyzewski

Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau-app-client.fly.dev/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/05/31/filip-intro.html b/blog/2022/05/31/filip-intro.html index 635ccf5a0b..222ebe5f1b 100644 --- a/blog/2022/05/31/filip-intro.html +++ b/blog/2022/05/31/filip-intro.html @@ -19,12 +19,12 @@ - - + +
-

Meet the team - Filip Sodić, Founding Engineer

· 6 min read
Matija Sosic

Welcome Filip!

Find Filip on GitHub.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

We are immensely excited to welcome Filip, our latest Founding Software +

Meet the team - Filip Sodić, Founding Engineer

· 6 min read
Matija Sosic

Welcome Filip!

Find Filip on GitHub.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

We are immensely excited to welcome Filip, our latest Founding Software Engineer! Filip is an experienced engineer and a passionate computer scientist - his two biggest passions are building compilers/designing programming languages and web development (what a lucky coincidence, right? @@ -78,7 +78,7 @@ projects because you think they aren’t ready yet. Good enough sometimes truly is good enough and things can often be considered done before you consider them done.

I still occasionally need to give this advice to myself :).

Lastly, where can people find or connect with you online?

GitHub: https://github.com/sodic

LinkedIn: https://www.linkedin.com/in/filipsodic/

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/06/01/gitpod-hackathon-guide.html b/blog/2022/06/01/gitpod-hackathon-guide.html index af932a902e..0ff94cd5ab 100644 --- a/blog/2022/06/01/gitpod-hackathon-guide.html +++ b/blog/2022/06/01/gitpod-hackathon-guide.html @@ -19,13 +19,13 @@ - - + +
-

How to win a hackathon. Brief manual.

· 4 min read

Wasp app deploye to Gitpod

"All good thoughts and ideas mean nothing without the proper tools to achieve them."
>Jason Statham

TL;DR: Wasp allows you to build and deploy a full-stack JS web app with a single config file. Gitpod spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Intro:

Usually, every hackathon starts from similar activities:

  1. setting up a local dev environment, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.
  2. building project backbone (folder structure, basic services, CRUD APIs, and so on).

Both of them are time-consuming, boring, and cause issues.

Dealing with routine might be frustrating

Thankfully, those issues can be avoided! Gitpod allows you to spin up a clean, already pre-set dev environment. And Wasp enables you to build a full-stack JS web app with a single config file (alongside your React and Node.js code). But first things first.

Pennywise luring into his openspace

Dev environment setup:

Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.

Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.

  1. Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.
  2. In your newly created repo, check the Readme and click the “Open in Gitpod” button
  3. Login via Github
  4. Allow pop-ups
  5. That’s it! Enjoy your fresh cloud-based dev environment!

Pennywise luring to take part in hackathon

An optional thing might be enabling the “Share” option to make the app accessible from the external internet.

How to share a workspace

You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.

Gitpod IDE types

So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.

Voila! 🥳

The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.

No need to fix local issues, ensure the Node version is aligned, or configure the deployment pipeline for DigitalOcean. Gitpod does all development preparations. The only thing the team has to do – is to implement the idea ASAP. And here Wasp comes into play!

Building project backbone:

Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!

The ideal case would be to:

  1. Check out the language overview: https://wasp-lang.dev/docs/general/language
  2. Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorial/create

It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.

And 20 minutes is time well spent to become more productive. Setting up each team member's environment locally likely takes more than 20 minutes if you don't use Gitpod.

To wrap up:

We think that Wasp + Gitpod is a powerful toolset for speedrunning any hackathon. No matter how complex or ambitious your project is. If it’s built with Node and React – nothing can stop you from winning. Good luck, have fun, and enjoy that pizza 🍕!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to win a hackathon. Brief manual.

· 4 min read

Wasp app deploye to Gitpod

"All good thoughts and ideas mean nothing without the proper tools to achieve them."
>Jason Statham

TL;DR: Wasp allows you to build and deploy a full-stack JS web app with a single config file. Gitpod spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Intro:

Usually, every hackathon starts from similar activities:

  1. setting up a local dev environment, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.
  2. building project backbone (folder structure, basic services, CRUD APIs, and so on).

Both of them are time-consuming, boring, and cause issues.

Dealing with routine might be frustrating

Thankfully, those issues can be avoided! Gitpod allows you to spin up a clean, already pre-set dev environment. And Wasp enables you to build a full-stack JS web app with a single config file (alongside your React and Node.js code). But first things first.

Pennywise luring into his openspace

Dev environment setup:

Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.

Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.

  1. Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.
  2. In your newly created repo, check the Readme and click the “Open in Gitpod” button
  3. Login via Github
  4. Allow pop-ups
  5. That’s it! Enjoy your fresh cloud-based dev environment!

Pennywise luring to take part in hackathon

An optional thing might be enabling the “Share” option to make the app accessible from the external internet.

How to share a workspace

You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.

Gitpod IDE types

So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.

Voila! 🥳

The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.

No need to fix local issues, ensure the Node version is aligned, or configure the deployment pipeline for DigitalOcean. Gitpod does all development preparations. The only thing the team has to do – is to implement the idea ASAP. And here Wasp comes into play!

Building project backbone:

Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!

The ideal case would be to:

  1. Check out the language overview: https://wasp-lang.dev/docs/general/language
  2. Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorial/create

It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.

And 20 minutes is time well spent to become more productive. Setting up each team member's environment locally likely takes more than 20 minutes if you don't use Gitpod.

To wrap up:

We think that Wasp + Gitpod is a powerful toolset for speedrunning any hackathon. No matter how complex or ambitious your project is. If it’s built with Node and React – nothing can stop you from winning. Good luck, have fun, and enjoy that pizza 🍕!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/06/15/jobs-feature-announcement.html b/blog/2022/06/15/jobs-feature-announcement.html index 20abdd7a11..84df18c498 100644 --- a/blog/2022/06/15/jobs-feature-announcement.html +++ b/blog/2022/06/15/jobs-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - Wasp Jobs

· 7 min read
Shayne Czyzewski

You get a job!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Storytime

Storytime

Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you’re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?

Spinning!

You wouldn’t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset.

The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!

src/server/workers/github.js
import axios from 'axios'
import { upsertMetric } from './utils.js'

export async function workerFunction() {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

const metrics = [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count },
{ name: 'Wasp GitHub Language', value: response.data.language },
{ name: 'Wasp GitHub Forks', value: response.data.forks },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues },
]

await Promise.all(metrics.map(upsertMetric))

return metrics
}

Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file.

Most jobs have a boss. Our first job executor is a... pg-boss. 😅

Eeek
Me trying to lay off the job-related puns. Ok, ok, I’ll quit. Ahhh!

In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery.

In the JavaScript world, Bull is quite popular these days. However, we decided to use pg-boss, as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack.

But isn’t a database as a queue an anti-pattern, you may ask? Well, historically I’d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective.

However, we will also continue to expand the number of job execution runtimes we support. Let us know in Discord what you’d like to see next!

Real Example - Updating Waspleau

If you are a regular reader of this blog (thank you, you deserve a raise! 😊), you may recall we created an example app of a metrics dashboard called Waspleau that used workers in the background to make periodic HTTP calls for data. In that example, we didn’t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge setupFn wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:

main.wasp
// A cron job for fetching GitHub stats
job getGithubStats {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/github.js"
},
schedule: {
cron: "*/10 * * * *"
}
}

// A cron job to measure how long a webpage takes to load
job calcPageLoadTime {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/loadTime.js"
},
schedule: {
cron: "*/5 * * * *",
args: {=json {
"url": "https://wasp-lang.dev",
"name": "wasp-lang.dev Load Time"
} json=}
}
}

And here is an example of how you can reference and invoke jobs on the server. Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.

src/server/serverSetup.js
/**
* These Jobs are automatically scheduled by Wasp.
* However, let's kick them off on server setup to ensure we have data right away.
*/
import { github } from '@wasp/jobs/getGithubStats.js'
import { loadTime } from '@wasp/jobs/calcPageLoadTime.js'

export default async function () {
await github.submit()
await loadTime.submit({
url: "https://wasp-lang.dev",
name: "wasp-lang.dev Load Time"
})
}

And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:

Architecture

For those interested, check out the full diff here and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!

Looks neat! What’s next?

First off, please check out our docs for Jobs. There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: https://github.com/wasp-lang/wasp/tree/release/examples/waspleau

In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!


Special thanks to Tim Jones for his hard work building an amazing OSS library, pg-boss, and for reviewing this post. Please consider supporting that project if it solves your needs!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - Wasp Jobs

· 7 min read
Shayne Czyzewski

You get a job!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Storytime

Storytime

Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you’re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?

Spinning!

You wouldn’t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset.

The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!

src/server/workers/github.js
import axios from 'axios'
import { upsertMetric } from './utils.js'

export async function workerFunction() {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

const metrics = [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count },
{ name: 'Wasp GitHub Language', value: response.data.language },
{ name: 'Wasp GitHub Forks', value: response.data.forks },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues },
]

await Promise.all(metrics.map(upsertMetric))

return metrics
}

Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file.

Most jobs have a boss. Our first job executor is a... pg-boss. 😅

Eeek
Me trying to lay off the job-related puns. Ok, ok, I’ll quit. Ahhh!

In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery.

In the JavaScript world, Bull is quite popular these days. However, we decided to use pg-boss, as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack.

But isn’t a database as a queue an anti-pattern, you may ask? Well, historically I’d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective.

However, we will also continue to expand the number of job execution runtimes we support. Let us know in Discord what you’d like to see next!

Real Example - Updating Waspleau

If you are a regular reader of this blog (thank you, you deserve a raise! 😊), you may recall we created an example app of a metrics dashboard called Waspleau that used workers in the background to make periodic HTTP calls for data. In that example, we didn’t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge setupFn wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:

main.wasp
// A cron job for fetching GitHub stats
job getGithubStats {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/github.js"
},
schedule: {
cron: "*/10 * * * *"
}
}

// A cron job to measure how long a webpage takes to load
job calcPageLoadTime {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/loadTime.js"
},
schedule: {
cron: "*/5 * * * *",
args: {=json {
"url": "https://wasp-lang.dev",
"name": "wasp-lang.dev Load Time"
} json=}
}
}

And here is an example of how you can reference and invoke jobs on the server. Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.

src/server/serverSetup.js
/**
* These Jobs are automatically scheduled by Wasp.
* However, let's kick them off on server setup to ensure we have data right away.
*/
import { github } from '@wasp/jobs/getGithubStats.js'
import { loadTime } from '@wasp/jobs/calcPageLoadTime.js'

export default async function () {
await github.submit()
await loadTime.submit({
url: "https://wasp-lang.dev",
name: "wasp-lang.dev Load Time"
})
}

And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:

Architecture

For those interested, check out the full diff here and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!

Looks neat! What’s next?

First off, please check out our docs for Jobs. There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: https://github.com/wasp-lang/wasp/tree/release/examples/waspleau

In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!


Special thanks to Tim Jones for his hard work building an amazing OSS library, pg-boss, and for reviewing this post. Please consider supporting that project if it solves your needs!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html b/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html index 9d4eb5633f..ad699a74c3 100644 --- a/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html +++ b/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html @@ -19,13 +19,13 @@ - - + +
-

ML code generation vs. coding by hand - what we think programming is going to look like

· 11 min read
Matija Sosic

We are working on a config language / DSL for building web apps that integrates with React & Node.js. A number of times we've been asked “Why are you bothering creating a new language for web app development? Isn’t Github Copilot* soon going to be generating all the code for developers anyhow?”.

This is on our take on the situation and what we think things might look like in the future.

Trending post!

This post was trending on HackerNews - you can see the discussion here.

Why (ML) code generation?

In order to make development faster, we came up with IDE autocompletion - e.g. if you are using React and start typing componentDid, IDE will automatically offer to complete it to componentDidMount() or componentDidLoad(). Besides saving keystrokes, maybe even more valuable is being able to see what methods/properties are available to us within a current scope. IDE being aware of the project structure and code hierarchy also makes refactoring much easier.

Although that’s already great, how do we take it to the next level? Traditional IDE support is based on rules written by humans and if we e.g. wanted to make IDE capable of implementing common functions for us, there would be just too many of them to catalogize and maintain by hand.

If there was only a way for a computer to analyze all the code we’ve written so far and learn by itself how to autocomplete our code and what to do about humanity in general, instead of us doing all the hard work ...

Delicious and moist cake aside, we actually have this working! Thanks to the latest advances in machine learning, IDEs can now do some really cool things like proposing the full implementation of a function, based on its name and the accompanying comments:

Copilot example - text sentiment
GitHub Copilot generating a whole function body based on its signature and the comments on top of it.

This is pretty amazing! The example above is powered by Github Copilot - it’s essentially a neural network trained on a huge amount of publicly available code. I will not get into the technical details of how it works under the hood, but there are lots of great articles covering the science behind it.

Seeing this, questions arise - what does this mean for the future of programming? Is this just IDE autocompletion on steroids or something more? Do we need to keep bothering with manually writing code, if we can just type in the comments what we want and that’s it?

Who maintains the code once it’s generated?

When thinking about how ML code generation affects the overall development process, there is one thing to consider that often doesn’t immediately spring to mind when looking at the impressive Copilot examples.

note

For the purposes of this post, I will not delve into the questions of code quality, security, legal & privacy issues, pricing, and others of similar character that are often brought up in these early days of ML code generation. Let’s just assume all this is sorted out and see what happens next.

The question is - what happens with the code once it is generated? Who is responsible for it and who will maintain and refactor it in the future?

Devs still need to maintain generated code

Although ML code generation helps with getting the initial code written, it cannot do much beyond that - if that code is to be maintained and changed in the future (and if anyone uses the product, it is), the developer still needs to fully own and understand it.

Imagine all we had was an assembly language, but IDE completion worked really well for it, and you could say “implement a function that sorts an array, ascending” and it would produce the required code perfectly. Would that still be something you’d like to return to in the future once you need to change your sort to descending 😅 ?

In other words, it means Copilot and similar solutions do not reduce the code complexity nor the amount of knowledge required to build features, they just help write the initial code faster, and bring the knowledge/examples closer to the code (which is really helpful). If a developer accepts the generated code blindly, they are just creating tech debt and pushing it forward.

Meet the big A - Abstraction 👆

If Github Copilot and others cannot solve all our troubles of learning how to code and understanding in detail how session management via JWT works, what can?

Abstraction - that’s how programmers have been dealing with the code repetition and reducing complexity for decades - by creating libraries, frameworks, and languages. It is how we advanced from vanilla JS and direct DOM manipulation to jQuery and finally to UI libraries such as React and Vue.

Introducing abstractions inevitably means giving up on a certain amount of power and flexibility (e.g. when summing numbers in Python you don’t get to exactly specify which CPU registers are going to be used for it), but the point is that, if done right, you don’t need nor want such power in the majority of the cases.

Abstraction equals less responsibility
What Uncle Ben actually meant: avoiding responsibility is the main benefit of abstraction! (Peter totally missed the point, unfortunately, and became Spiderman instead of learning how to code)

The only way not to be responsible for a piece of code is that it doesn’t exist in the first place.

Because as soon as pixels on the screen change their color it’s something you have to worry about, and that is why the main benefit of all frameworks, languages, etc. is less code == less decisions == less responsibility.

The only way to have less code is to make less decisions and provide fewer details to the computer on how to do a certain task - ideally, we’d just state what we want and we wouldn’t even care about how it is done, as long as it’s within the time/memory/cost boundaries we have (so we might need to state those as well).

Let’s take a look at the very common (and everyone’s favorite) feature in the world of web apps - authentication (yaay ☠️ 🔫)! The typical code for it will look something like this:

Auth on the backend in Node.js - example
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'

import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import config from '../config.js'

const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)

const JWT_SECRET = config.auth.jwtSecret

export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)

const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
if (!authHeader) {
return next()
}

if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7, authHeader.length)

let userIdFromToken
try {
userIdFromToken = (await verify(token)).id
} catch (error) {
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
return res.status(401).send()
} else {
throw error
}
}

const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
if (!user) {
return res.status(401).send()
}

const { password, ...userView } = user

req.user = userView
} else {
return res.status(401).send()
}

next()
})

const SP = new SecurePassword()

export const hashPassword = async (password) => {
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
return hashedPwdBuffer.toString("base64")
}

export const verifyPassword = async (hashedPassword, password) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}

And this is just a portion of the backend code (and for the username & password method only)! As you can see, we have quite a lot of flexibility here and get to do/specify things like:

  • choose the implementation method for auth (e.g. session or JWT-based)
  • choose the exact npm packages we want to use for the token (if going with JWT) and password management
  • parse the auth header and specify for each value (Authorization, Bearer, …) how to respond
  • choose the return code (e.g. 401, 403) for each possible outcome
  • choose how the password is decoded/encoded (base64)

On one hand, it’s really cool to have that level of control and flexibility in our code, but on the other hand, it’s quite a lot of decisions (== mistakes) to be made, especially for something as common as authentication!

If somebody later asks “so why exactly did you choose secure-password npm package, or why exactly base64 encoding?” it’s something we should probably answer with something else rather than “well, there was that SO post from 2012 that seemed pretty legit, it had almost 50 upvotes. Hmm, can’t find it now though. Plus, it has ‘secure’ in the name, that sounds good, right?

Another thing to keep in mind is that we should also track how things change over time, and make sure that after a couple of years we’re still using the best practices and that the packages are regularly updated.

If we try to apply the principles from above (less code, less detailed instructions, stating what we want instead of how it needs to be done), the code for auth might look something like this:

auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/dashboard"
}

Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually works in Wasp).

We don’t need to care what exact packages or encryption methods are used beneath the hood - it is the responsibility we trust with the authors and maintainers of the abstraction layer, just like we trust that Python knows the best how to sum two numbers on the assembly level and that it is kept in sync with the latest advancements in the field. The same happens when we rely on the built-in data structures or count on the garbage collector to manage our program’s memory well.

But my beautiful generated codez 😿💻! What happens with it then?

Don’t worry, it’s all still here and you can generate all the code you wish! The main point to understand here is that ML code generation and framework/language development complement rather than replace each other and are here to stay, which is ultimately a huge win for the developer community - they will keep making our lives easier and allow us to do more fun stuff (instead of implementing auth or CRUD API for the n-th time)!

I see the evolution here as a cycle (or an upward spiral in fact, but that’s beyond my drawing capabilities):

  1. language/framework exists, is mainstream, and a lot of people use it
  2. patterns start emerging (e.g. implementing auth, or making an API call) → ML captures them, offers via autocomplete
  3. some of those patterns mature and become stable → candidates for abstraction
  4. new, more abstract, language/framework emerges
  5. back to step 1.

Language evolution lifecycle
It’s the circle of (language) life, and it moves us all - Ingonyama nengw' enamabala, …

Conclusion

This means we are winning on both sides - when the language is mainstream we can benefit from ML code generation, helping us write the code faster. On the other hand, when the patterns of code we don’t want to repeat/deal with emerge and become stable we get a whole new language or framework that allows us to write even less code and care about fewer implementation details!

Fizz Buzz with Copilot - stop
The future is now, old man.

*Not to be biased, there are also other solutions offering similar functionality - e.g. TabNine, Webstorm has its own, Kite, GPT Code Clippy (OSS attempt) et al., but Github Copilot recently made the biggest splash.

Writing that informed this post

Thanks to the reviewers

Jeremy Howard, Maxi Contieri, Mario Kostelac, Vladimir Blagojevic, Ido Nov, Krystian Safjan, Favour Kelvin, Filip Sodic, Shayne Czyzewski and Martin Sosic - thank you for your generous comments, ideas and suggestions! You made this post better and made sure I don't go overboard with memes :).

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

ML code generation vs. coding by hand - what we think programming is going to look like

· 11 min read
Matija Sosic

We are working on a config language / DSL for building web apps that integrates with React & Node.js. A number of times we've been asked “Why are you bothering creating a new language for web app development? Isn’t Github Copilot* soon going to be generating all the code for developers anyhow?”.

This is on our take on the situation and what we think things might look like in the future.

Trending post!

This post was trending on HackerNews - you can see the discussion here.

Why (ML) code generation?

In order to make development faster, we came up with IDE autocompletion - e.g. if you are using React and start typing componentDid, IDE will automatically offer to complete it to componentDidMount() or componentDidLoad(). Besides saving keystrokes, maybe even more valuable is being able to see what methods/properties are available to us within a current scope. IDE being aware of the project structure and code hierarchy also makes refactoring much easier.

Although that’s already great, how do we take it to the next level? Traditional IDE support is based on rules written by humans and if we e.g. wanted to make IDE capable of implementing common functions for us, there would be just too many of them to catalogize and maintain by hand.

If there was only a way for a computer to analyze all the code we’ve written so far and learn by itself how to autocomplete our code and what to do about humanity in general, instead of us doing all the hard work ...

Delicious and moist cake aside, we actually have this working! Thanks to the latest advances in machine learning, IDEs can now do some really cool things like proposing the full implementation of a function, based on its name and the accompanying comments:

Copilot example - text sentiment
GitHub Copilot generating a whole function body based on its signature and the comments on top of it.

This is pretty amazing! The example above is powered by Github Copilot - it’s essentially a neural network trained on a huge amount of publicly available code. I will not get into the technical details of how it works under the hood, but there are lots of great articles covering the science behind it.

Seeing this, questions arise - what does this mean for the future of programming? Is this just IDE autocompletion on steroids or something more? Do we need to keep bothering with manually writing code, if we can just type in the comments what we want and that’s it?

Who maintains the code once it’s generated?

When thinking about how ML code generation affects the overall development process, there is one thing to consider that often doesn’t immediately spring to mind when looking at the impressive Copilot examples.

note

For the purposes of this post, I will not delve into the questions of code quality, security, legal & privacy issues, pricing, and others of similar character that are often brought up in these early days of ML code generation. Let’s just assume all this is sorted out and see what happens next.

The question is - what happens with the code once it is generated? Who is responsible for it and who will maintain and refactor it in the future?

Devs still need to maintain generated code

Although ML code generation helps with getting the initial code written, it cannot do much beyond that - if that code is to be maintained and changed in the future (and if anyone uses the product, it is), the developer still needs to fully own and understand it.

Imagine all we had was an assembly language, but IDE completion worked really well for it, and you could say “implement a function that sorts an array, ascending” and it would produce the required code perfectly. Would that still be something you’d like to return to in the future once you need to change your sort to descending 😅 ?

In other words, it means Copilot and similar solutions do not reduce the code complexity nor the amount of knowledge required to build features, they just help write the initial code faster, and bring the knowledge/examples closer to the code (which is really helpful). If a developer accepts the generated code blindly, they are just creating tech debt and pushing it forward.

Meet the big A - Abstraction 👆

If Github Copilot and others cannot solve all our troubles of learning how to code and understanding in detail how session management via JWT works, what can?

Abstraction - that’s how programmers have been dealing with the code repetition and reducing complexity for decades - by creating libraries, frameworks, and languages. It is how we advanced from vanilla JS and direct DOM manipulation to jQuery and finally to UI libraries such as React and Vue.

Introducing abstractions inevitably means giving up on a certain amount of power and flexibility (e.g. when summing numbers in Python you don’t get to exactly specify which CPU registers are going to be used for it), but the point is that, if done right, you don’t need nor want such power in the majority of the cases.

Abstraction equals less responsibility
What Uncle Ben actually meant: avoiding responsibility is the main benefit of abstraction! (Peter totally missed the point, unfortunately, and became Spiderman instead of learning how to code)

The only way not to be responsible for a piece of code is that it doesn’t exist in the first place.

Because as soon as pixels on the screen change their color it’s something you have to worry about, and that is why the main benefit of all frameworks, languages, etc. is less code == less decisions == less responsibility.

The only way to have less code is to make less decisions and provide fewer details to the computer on how to do a certain task - ideally, we’d just state what we want and we wouldn’t even care about how it is done, as long as it’s within the time/memory/cost boundaries we have (so we might need to state those as well).

Let’s take a look at the very common (and everyone’s favorite) feature in the world of web apps - authentication (yaay ☠️ 🔫)! The typical code for it will look something like this:

Auth on the backend in Node.js - example
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'

import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import config from '../config.js'

const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)

const JWT_SECRET = config.auth.jwtSecret

export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)

const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
if (!authHeader) {
return next()
}

if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7, authHeader.length)

let userIdFromToken
try {
userIdFromToken = (await verify(token)).id
} catch (error) {
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
return res.status(401).send()
} else {
throw error
}
}

const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
if (!user) {
return res.status(401).send()
}

const { password, ...userView } = user

req.user = userView
} else {
return res.status(401).send()
}

next()
})

const SP = new SecurePassword()

export const hashPassword = async (password) => {
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
return hashedPwdBuffer.toString("base64")
}

export const verifyPassword = async (hashedPassword, password) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}

And this is just a portion of the backend code (and for the username & password method only)! As you can see, we have quite a lot of flexibility here and get to do/specify things like:

  • choose the implementation method for auth (e.g. session or JWT-based)
  • choose the exact npm packages we want to use for the token (if going with JWT) and password management
  • parse the auth header and specify for each value (Authorization, Bearer, …) how to respond
  • choose the return code (e.g. 401, 403) for each possible outcome
  • choose how the password is decoded/encoded (base64)

On one hand, it’s really cool to have that level of control and flexibility in our code, but on the other hand, it’s quite a lot of decisions (== mistakes) to be made, especially for something as common as authentication!

If somebody later asks “so why exactly did you choose secure-password npm package, or why exactly base64 encoding?” it’s something we should probably answer with something else rather than “well, there was that SO post from 2012 that seemed pretty legit, it had almost 50 upvotes. Hmm, can’t find it now though. Plus, it has ‘secure’ in the name, that sounds good, right?

Another thing to keep in mind is that we should also track how things change over time, and make sure that after a couple of years we’re still using the best practices and that the packages are regularly updated.

If we try to apply the principles from above (less code, less detailed instructions, stating what we want instead of how it needs to be done), the code for auth might look something like this:

auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/dashboard"
}

Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually works in Wasp).

We don’t need to care what exact packages or encryption methods are used beneath the hood - it is the responsibility we trust with the authors and maintainers of the abstraction layer, just like we trust that Python knows the best how to sum two numbers on the assembly level and that it is kept in sync with the latest advancements in the field. The same happens when we rely on the built-in data structures or count on the garbage collector to manage our program’s memory well.

But my beautiful generated codez 😿💻! What happens with it then?

Don’t worry, it’s all still here and you can generate all the code you wish! The main point to understand here is that ML code generation and framework/language development complement rather than replace each other and are here to stay, which is ultimately a huge win for the developer community - they will keep making our lives easier and allow us to do more fun stuff (instead of implementing auth or CRUD API for the n-th time)!

I see the evolution here as a cycle (or an upward spiral in fact, but that’s beyond my drawing capabilities):

  1. language/framework exists, is mainstream, and a lot of people use it
  2. patterns start emerging (e.g. implementing auth, or making an API call) → ML captures them, offers via autocomplete
  3. some of those patterns mature and become stable → candidates for abstraction
  4. new, more abstract, language/framework emerges
  5. back to step 1.

Language evolution lifecycle
It’s the circle of (language) life, and it moves us all - Ingonyama nengw' enamabala, …

Conclusion

This means we are winning on both sides - when the language is mainstream we can benefit from ML code generation, helping us write the code faster. On the other hand, when the patterns of code we don’t want to repeat/deal with emerge and become stable we get a whole new language or framework that allows us to write even less code and care about fewer implementation details!

Fizz Buzz with Copilot - stop
The future is now, old man.

*Not to be biased, there are also other solutions offering similar functionality - e.g. TabNine, Webstorm has its own, Kite, GPT Code Clippy (OSS attempt) et al., but Github Copilot recently made the biggest splash.

Writing that informed this post

Thanks to the reviewers

Jeremy Howard, Maxi Contieri, Mario Kostelac, Vladimir Blagojevic, Ido Nov, Krystian Safjan, Favour Kelvin, Filip Sodic, Shayne Czyzewski and Martin Sosic - thank you for your generous comments, ideas and suggestions! You made this post better and made sure I don't go overboard with memes :).

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html b/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html index b2e01ca168..d47ed57c6b 100644 --- a/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html +++ b/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html @@ -19,13 +19,13 @@ - - + +
-

How to communicate why your startup is worth joining

· 31 min read
Vasili Shynkarenka

Except for a handful of companies who send people to Mars or develop AGI, most startups don’t seem to offer a good reason to join them. You go to their websites and all you see is vague, baseless, overly generic mission-schmission/values-schvalues HR nonsense that supposedly should turn you into a raving fan of whatever they’re doing and make you hit that “Join” button until their servers crash. Well…

Some people think that’s because most startups aren’t worth joining. I disagree. This argument generalizes one’s own reasons for joining a startup onto every other human being out there, which is unlikely to be true. I think most startups, no matter how ordinary, do have a reason to join them; a good reason; even many good reasons — they just fail to communicate them well. They’re like a shy nerd on Tinder with an empty bio and no profile pic: a kind, intelligent, and thoughtful human being who, unfortunately, will be ruthlessly swiped left — not because he’s a bad match but because his profile doesn’t show why he’s a good one.

Visually, this “Tinder profile problem” looks like this:

Illustration of candidates not seeing why to join a startup

Now, look what would happen if a startup communicated a bit better. Suddenly, our candidates could see a reason to join. If the reason is good, they might even swipe right.

Illustration of candidates seeing one reason to join a startup

But most startups have many good reasons to join them. If only they communicated well, the outcome would be something like this:

Illustration of candidates seeing many reasons to join; one candidate already running for it

Now, you’re probably wondering just what exactly those reasons are.

Here’s a rough list:

  1. The founders are interesting / fun / smart / human / you name it
  2. The team is great
  3. The culture is amazing
  4. The business is doing well

However, if you just copy this list and paste it on your jobs page, you will accomplish nothing. The candidates will never believe you. What you need to do instead is to supply them with a system of concretes (facts) from which their minds will form these abstract conclusions.

For example:

  • Instead of declaring that “the founders are reflective, thoughtful, and persistent,” show them how so, like Sarah from Canny does by writing comprehensive year-in-review blog posts for four years in a row.
  • Instead of proclaiming that “the founders are humble and can have fun,” show them how so, like Michael from Fibery did by becoming a hero of this hilarious page. (No businessy founder would ever agree to make this public. Michael did.)
  • Instead of purporting that “the team is great” or “you’ll work alongside very smart people” (God, I hate that one!), show them who exactly those people are, as PostHog does here and Wasp does here and here.

In the rest of the post, I’ll go through the four broad reasons to join a startup one by one and show real-life examples of communicating them well. In the end, I will explain how these four reasons, communicated well, fuse into two compelling messages that will interest any candidate.

One last thing. For the sake of clarity and comprehension, I will write in the second person. Instead of saying “candidates would never believe them,” I will say “you would never believe them.” It’s much easier to read and understand.

Possible reasons why your startup is worth joining, and how to communicate them well

1. FOUNDERS — or, the founders are interesting / fun / smart / human / you name it

Most startups have curious, interesting, ambitious, terribly smart founders; the kind most of us would love to work for if we had a chance. Sadly, only a few leverage this asset. In most cases, all you get is a small round pic with a fancy title and a few abstract, high-level sentences that cause no excitement whatsoever. What a shame!

How Canny commmunicates who their founders are

Founder Stories blog category

The first notable thing Canny does is the Founder Stories category in their blog. By quickly skimming the posts, you can understand that Sarah and Andrew (the founders):

If they just pinned this list of virtues to their Jobs page, you would never believe them. Instead, Sarah and Andrew show what actions they take, how they work, how they think, how they live — and you make up their own mind about what kind of people Sarah and Andrew are from seeing all that. The difference is enormous.

Note their writing style. They don’t claim to be know-it-alls with titles like “How to bootstrap your startup.” Instead, they write “How we Bootstrapped our SaaS Startup to Ramen Profitability.” They cover only what they know instead of overgeneralizing. This shows both expertise and humility.

A screenshot of Canny's Founder Stories blog category

Personal Instagram

The second thing Sarah and Andrew do well to communicate who they are is their Instagram. They don’t post glamorous keynote appearances, as many entrepreneurs do. They share the actual day-to-day working life — both the fun and the struggle. It gives you a good idea of what they’re after in life. (Not keynotes.) That’s why it works, and that’s why people love it.

A photo from Sarah and Andrew's personal Instagram

Side note: Sarah explains how she develops the Canny brand in this post. If you want to build a good one, give it a read. She also wrote about how they attract top talent. You can read it here.

How Fibery communicates who their founder is

Startup Diary blog post series

While you can get a pretty good idea of Michael (the founder) from the hilarious “Remote” page Fibery shipped last year, his Startup Diary post series offers an even better insight into his soul. In these monthly posts, Michael honestly shares everything that’s going on with Fibery, including the good, the bad, and the ugly: firing people for poor performance, losing important customers, and failing to reach product-market fit. The fact that he’s already written 45 of those (as of Aug 2022) is also telling. And he’s not a native English speaker. If he can do that, why can’t you?

A screenshot of Fibery's Startup Diary blog category

Crazy challenges

Besides writing the Startup Diary, Michael also embarks on crazy challenges like writing 100 posts about products. Only a passionate, driven person would commit to such a thing. You cannot help but respect him for it. (Before this challenge, he wrote 100 Medium posts in 100 days in 2018. You can read them here. Just scroll a few screens to reach the old stuff.)

A screenshot of Fibery's 100 posts about products blog category

If you look carefully, you’ll notice that Michael’s thinking about building a company is different from Sarah’s. For example, he despises the gentle, soothing “Oh don’t worry that it didn’t work out; you did such a good work!” approach, which is ubiquitous in the modern startup world. Instead, he states that dissatisfaction leads to progress, referring to the famous “Not quite my tempo” scene from Whiplash. Does that make you like him more than Sarah?

It depends. If you believe that being soft and balanced is better, you’ll go with Sarah; if you believe that real progress comes only from working yourself to the bone, you’ll go with Michael (or Elon). The important thing is that both founders have their own, unique viewpoints of how things should be done, and that they communicate these viewpoints as-is instead of chopping their legs off to fit the latest Procrustean fad.

In-depth, original blog posts about the industry

Some entrepreneurs say that doing a startup is like “jumping off a cliff and building your wings on the way down.” Some of it might be true. But if you want reasonable people to jump with you, you better tell them that you have a degree in engineering and know how to assemble wings in a free fall. Otherwise, the only team you’ll recruit is a suicide squad looking for a splashy hit.

To communicate his expertise, Michael writes in-depth, original, theoretical posts about the nature of knowledge management and organizational productivity. These posts are gems, both literally and metaphorically. (They’re filed under the Gems category in the Fibery blog.)

For example:

After reading these articles, you understand not only that Michael really knows how to build wings while falling off the cliff, but that he has already jumped a few times. (Prior to Fibery, Michael had worked on knowledge management for more than a decade. He also had built a successful project management software, Targetprocess.) You know that he’s an expert who can be trusted.

Interestingly, even though Michael writes differently from Sarah, they both leverage what they’re good at. Sarah does not try to produce treatises on software development philosophy, and Michael doesn’t gush out with his personal learnings from building a startup. That, I think, is the right way to do it.

How PostHog communicates who their founders are

PostHog’s founders James and Tim don’t write 100 posts in 100 days or run a personal Instagram. But they’ve come up with something else to communicate what kind of people they are. And it’s something unique.

Well-written, concise bio

First, both founders have decent profiles in the company handbook. These bios are short, clear, and humane. They’re also very specific. Where else have you seen the name of the CEO’s cat?

A screenshot of James Hawkins' bio in the PostHog Handbook

Personal README files

Second, both James and Tim have an extensive README file (one, two) on how to work with them. These files give you an insight into their productivity habits, interests, and quirks. In fact, after reading them, you will likely have a better idea of the founders than you’d usually get from working at a company for a month!

For instance, James’s file has sections like:

  • Short bio. Includes very specific details like: “I tend to work 9am to 5pm with an hour for lunch, then I have a gap to have dinner with my family, then 9pm to around 11pm ish.”
  • Very clear areas of responsibility. No need to wonder what the hell the CEO is doing anymore!
  • Quirks. These are remarkably humble and open-minded, like:
    • “If I haven’t responded to something that you’ve sent me, that’s probably because I’ve read it and don’t feel particularly strongly - so just make a call on what to do if you don’t hear back in a reasonable time frame.”
    • “I’m a little disorganized. I compensate for this by making sure the teams I work on have this skill. Often I think this actually helps me prioritize the things that really matter.”
    • Explaining these quirks is an ingenious move. Besides explaining how to work with James, this section communicates that he’s profoundly self-aware and willing to accept and leverage his weaknesses. These qualities are very rare and incredibly valuable.
  • What I value. In stark contrast to most HR nonsense, these values are very clear, very specific, and written in English rather than HRese. (I just came up with this term: it means “legalese but for HR.”) Here are two examples:
    • “Proactivity. Do not ask me for permission to do things - I wouldn’t have hired you if I didn’t trust you. I’d rather 9 things get done well and 1 thing I disagree with than we don’t get anything done at all.”
    • “Directness impresses me. If you don’t like something please just say so. It makes for much healthier relationships.”

In addition to that, there’s also: How I can help you, How you can help me, My goals until end December 2022 (very specific!), Personal strategy, Execution todo (including “1 bike ride a week”!) and Archived todo.

In summary, this README page is a gem. I wish more founders had them.

A screenshot of James Hawkins' README in the PostHog Handbook

How we at Wasp communicate who our founders are

“Who we are” section of every job description page

Matija and Martin (the founders of Wasp) embedded a concise description of who they are right into each job description page in Notion. They knew that this is the first company artifact many candidates will see. So they saved candidates time and effort on digging up who the hell started Wasp.

Note the language and substance of this list. When you read it, you immediately get a sense of who Matija and Martin are as people — fun, easygoing, no-corporate-bullshit kinda guys. Now imagine it said something “more normal,” like: “The company was founded by seasoned entrepreneurs…” What impression would that make?

A screenshot of Wasp's job description page

2. TEAM — or, the team is great

It is startling how little most startups tell you about their teams. Often all you get is a chessboard of faces and titles, which gives you no idea who these people are as people or how working with them will feel like. Given how crucial a reason “great team” is for most candidates, improving how you communicate it seems like a low-hanging fruit.

How Canny communicates who is on their team

Decent team page

The Canny’s difference starts with a team page. It has a dense summary of who each team member is as a person and includes high-quality, lively photos of everybody.

A screenshot of Canny's Team page

Look how specific those bios are. In most cases, all you get here is a generic “developer” or “marketer” without any personal details. Bios of robots, not people. No wonder nothing comes to mind, except perhaps for Agent Smith. But Canny’s bios are different. When you read them, you can actually imagine the person! They’re Neos in the world of Smiths.

Remarkable “Why work at Canny” blog post

From there, it gets only better. Canny’s chief weapon for explaining their team is a blog post, the “Why work at Canny” blog post. Sarah wrote it back in the summer of 2021. It is full of quotes from team members and photos of their workdays and vacations. Real photos of real people. No wonder the comments section under the post abounds with raving fans willing to join the team straight away!

A screenshot of comments under the Canny's Why work at Canny blog post

Perhaps the best thing about this post is how little work it takes to create one. I imagine that collecting the data took some time, but the actual writing (it’s an 11-min read) took no more than a week. A week of work for a candidate magnet of such tremendous power? Sounds like a deal.

P.s. Sarah writes a lot more about their team in her yearly review posts, but I decided not to elaborate on those for the sake of clarity. You can check them out here: year 1, year 2, year 3, and year 4.

How Fibery communicates who is on their team

Weird About Us page

Unlike Canny and PostHog’s, Fibery’s About Us page doesn’t reveal much info about each team member. You will find no bios or README files there. But it clearly tells you one thing: the team is a bunch of weirdos. So, if weird is your thing, you’ll be attracted to Fibery like a moth to a flame. (Side note: Fibery managed to clearly explain their vision in one paragraph. This is rare.)

A screenshot of Fibery's About Us page

I’ve already mentioned Michael’s Startup Diary monthly blog series. What I didn’t say is that each post communicates something about the team: who did what that month, random Slack posts (links, quotes, tweets, and images), etc. If someone new joined that month, Michael writes a few paragraphs explaining who that person is, where they come from, what they’re going to do at Fibery, and even attaches a photo. Like Chris.

A screenshot of Fibery's Startup Diary blog post

How PostHog communicates who is on their team

Team section in the company handbook

At PostHog, every team member has a well-written, few-paragraphs-long bio and a stylish illustration on the Team section of the PostHog’s Handbook. (Which is a work of art worthy of its own blog post, by the way.) Many team members have their own README files, like the founders do. Check out Lottie Coxon’s, PostHog’s Graphic Designer’s README here, and some others here and here. Even a quick read through these bios and READMEs gives you a good idea of who PostHog has on board.

A screenshot of PostHog's team section in the handbook

Another screenshot of PostHog's team section in the handbook

Day-in-life videos from employees

In addition to bios and READMEs, PostHog has a day-in-life video of Lottie, their graphic designer. It communicates a lot more information about what kind of person she is and how working at PostHog feels like than her bio. I wish they had more of those.

A screenshot from PostHog's graphic designer day-in-life video

Finally, PostHog’s handbook offers two more sections where candidates can learn even more about the team: Culture and Team structure. All are worth a read, and each tells you something new about the company and the team, nurturing your liking and respect for these people. Definitely worth stealing.

How we at Wasp communicate who is on our team

“Meet the team” blog posts

To help candidates understand who they will be working with, we at Wasp write a blog post about each new hire:

The posts are brief enough to be read in one sitting. Yet, they are very informative. Basically, each post is an interview, presented as an article. We hope they give candidates a good idea of who they'll be spending half of their waking time with.

A screenshot of Wasp's Meet the team blog post

3. CULTURE — or, the culture is amazing

While researchers still argue about the ultimate definition, most of us understand culture as “what working here feels like” and/or “how we do things here.” We also understand how crucial it is for those looking for work. It seems glaringly obvious that startups should work hard on communicating their culture. Yet, most companies don’t. Or, even worse, they flood their websites with meaningless HR fluff, which only scares interesting people away. In short, communicating culture well is another low-hanging fruit waiting to be picked.

How Canny communicates their culture

Canny does an outstanding job at communicating their culture. The primary tool they employ is, once again, their blog. (Note how multifunctional it is: founders, expertise, team, and now culture.) The posts in the Founder Stories category convey very well what working at Canny feels like. Here are a few examples.

“Why work at Canny” blog post

I’ll risk repeating myself, but this post so beautifully explains Canny’s culture that I couldn’t resist. It mentions why and how they work remotely, how they do team retreats (with photos and a video from Lisbon!), and how they had fun together playing weird Zoom games when travel was not an option due to Covid.

Pay attention to the imagery. It communicates a lot more information than any lengthy, elaborate description would. Indeed, a picture is often worth a thousand words.

A photo of Canny's two team members hacking in Denver

“Lessons from a year of team retreats” blog post

Instead of saying that “team is our priority” or “we invest in our people,” Sarah shows what they’ve done to support their team.

Again, note how specific the imagery is.

A photo from Canny's Lessons from a year of team retreats blog post

Interestingly, Sarah’s post isn’t framed as “hey we do many team retreats, we’re awesome, come work for us.” If they wrote that, the reader would feel uneasy. They would sense bragging. That’s why the explicit message in the post is what Canny learned doing team retreats, not that they’ve done many. This explicit message, however, implies that they indeed have done many retreats! It sends a message that Canny cares for their employees without explicitly saying so. This is what true mastery looks like.

“The end of our digital nomad journey” blog post

Although this post describes Sarah and Andrew’s personal nomad experience, Sarah managed to reveal Canny’s culture through it. To do that, she described how the team worked on Canny during those nomad years. She also wrote about their communication struggles, routines, and a lot more. And, again, look at how effectively her seemingly imperfect screenshots and photos transmit the vibe!

A photo from Canny's The end of our digital nomad journey blog post

Another photo from Canny's The end of our digital nomad journey blog post

How Fibery communicates their culture

While Fibery’s culture is different from Canny’s, they also communicate it well. Their primary tool is a weird, quirky website full of special projects that give you a sense of how they do things at Fibery and what working there feels like.

Anxiety page

The first project is Fibery’s /anxiety page. Launched in 2019, it mocks every serious enterprise software out there with puns like “Yet another collaboration tool” as the page title, “Mistake” as a sign-up button text, and, my favorite, “Try—Suffer—Quit” page structure.

A screenshot of Fibery's /anxiety page

One day three years ago, someone submitted this page to Hacker News. The post surged to the top of the frontpage, stayed there for many hours, and got 705 upvotes and 145 comments from people all over the world relating to Fibery’s culture. Why? Because it felt real.

Here’s a glimpse of what people wrote in the comments:

A screenshot of Hacker News comments on Fibery's /anxiety page

Another screenshot of Hacker News comments on Fibery's /anxiety page

Remote page

The second special project Fibery did to communicate their culture is the /remote page. It shows what working from home is really like. It’s the funniest thing I’ve ever seen done by a software startup. (Have you ever seen a CEO being licked by a dog?) It also shows how the Fibery team works and even how they use Fibery to build Fibery. Like Canny’s “Lessons from a year of team retreats” blog post, it does so implicitly. A true masterpiece.

Weird, humorous site

Broadly, the whole site screams that Fibery is a place for misfits, rebels, and trouble makers; the place where such people will be valued and will feel like home; the place built around brutal honesty and spicy humor.

The “What (non-)customers say” section is worth a mention. Over my nine years in startups, I haven’t seen a site that a) lists bad customer reviews; and b) uses 💩 emoji as a filter. Again, this is telling. It says a lot about who they are as people: humble, real, and fond of humor.

A screenshot of Fibery's About Us page, What non-customers say section

How PostHog communicates their culture

Comprehensive company handbook covering all-things culture

PostHog’s way of communicating their culture is the most explicit of all four examples, yet very effective. Their primary tool is the PostHog Handbook, which covers virtually every aspect of what working at PostHog feels like: interviews, onboarding, training, management, communication, and even firing. (They call it offboarding.)

The handbook goes all the way up to the high-level strategy, which is very clear. Notably, PostHog’s strategy section not only puts forth ambitious goals but actually explains how exactly the company will get there.

The values section is very specific; perhaps the most specific I’ve ever seen. PostHog does not merely list their values as meaningless abstractions but supports them with evidence. Some values have many paragraphs of examples demonstrating how the team follows them.

A screenshot of the Values section in the PostHog's handbook

They also have a specific Culture page with a 5-minute video from the CEO explaining how they designed PostHog for remote work from day one, which nicely complements the text.

A screenshot from James Hawkins's video

In summary, if Canny’s weapon of choice is the blog and Fibery’s is the website, then PostHog’s is definitely the handbook. It’s a work of art.

How we at Wasp communicate our culture

Easygoing vibe from memes, copy, and imagery

Unlike Posthog, we at Wasp don’t (yet) have a dedicated Culture page. We are too small for that. But that doesn’t stop us from showing what working at Wasp feels like. We just use different tools.

Our Twitter, blog, and monthly updates abound with memes, GIFs, and hilarious imagery. Plus, we write them in a humorous, lighthearted, easygoing style. By just scrolling through these things for a few minutes, candidates can understand that we aren’t some corporate bros. And if they like working on interesting things while having fun, they won’t help but feel an inkling to reach out.

A funny image from Wasp's blog post about GitHub Copilot

A photo of Wasp's team packing t-shirts for users

4. PROGRESS — or, the business is doing well

When you just closed an $80 million Series B or signed Facebook as a customer, communicating progress is easy. You just state these facts. However, most companies need to attract great people way before Series B. In fact, it is these very people who’re going to get you there. As most startups are secretive about how things are going, communicating that things are going somehow — no matter how negligible your progress in contrast to the big guys — becomes quite an advantage. It immediately de-risks the opportunity in the candidate’s eyes. So, if EXPERTISE is about convincing candidates that you know how to build the wings, PROGRESS is about showing them the half-built carcass on your way down. Both are important if you want great people to jump off the cliff with you.

How Canny communicates their progress

To give candidates a sense that things are moving, that this company is not some long slog but a place where progress is made every day, that they can become a part of something that’s growing and, therefore, can grow themselves, to do all that, Canny does two things.

“Year in review” blog posts

The first one is their “Year in review” blog post series. Such comprehensive, thoughtful reviews are rare in the startup world. What is even rarer is when these posts span over four consecutive years. It sends a message that the founders are persistent and devoted to making this company successful.

Below are all Canny’s year-in-review posts in a sequential order:

A screenshot of Canny's Year in review blog post

Important revenue milestones blog posts

In addition to year-in-review posts, Sarah writes about hitting notable revenue milestones. Like with yearly reviews, such transparency is rare. It attracts attention, causes liking, and builds trust.

For example:

A screenshot of Canny's How we built a $1m ARR SaaS startup blog post

Short tweets with progress summary

Finally, Sarah occasionally tweets short summaries of their progress, like this one. These tweets work like ads. Over time, a candidate’s brain fuses them into a broader idea like “Canny is growing” or “Canny is doing well.” Then, once a candidate decides to change jobs, it nudges the candidate to consider Canny.

A screenshot of Sarah’s tweet with progress update

How Fibery communicates their progress

Startup Diary blog posts

The most notable thing Fibery does to communicate their progress is the Startup Diary blog posts series written by the founder, Michael, every month, for the past 45 months. It’s the longest series of monthly updates I know. In these posts, Michael honestly shares everything that’s going on with the company: the good, the bad, and the ugly.

Below are just a few examples, selected by me. You can study all Fibery’s monthly updates here.

  • #2 Slow September 2018 — Fibery startup progress in September 2018. Slow month with not so many news. First positive feedback. Company name selection.
  • #6 Planning Private Beta in January 2019 — Fibery startup progress in January 2019: Private beta goals, selecting a market positioning (hard), apps re-design.
  • #10 Burn in May 2019 — Several people burned out, new features are delivered, public release will be sooner (we hope) (despite ill fortune).
  • #16 Crazy November 2019 — Fibery 1.0 is silently launched. Silence is hard to keep. HackerNews front page. Twitter madness. 3000 registered accounts.
  • #17 Fragmented December 2019 — Public announcements moved to January. +Lena. Tons of feedback. First money! Hype is over. We consider rising a ~$4M round.
  • #35 Raised $3.1M in July 2021 — TLDR: We closed $3.1M seed round. Building a second brain for teams. Fibery mission. Building in Public. Automation rules. Documents and Rich Text history.
  • #36 20k MRR in August 2021 — Special Startup Diary edition. 20k MRR & 15 new customers! +Chris. +Sales agency. 4 case studies. Airtable integration & notify people action.
  • ($30K MRR) #42 Connecting the dots in April 2022 — TLDR: 🇺🇦 Ukrainian war affected our performance. $30K MRR 🐌. 69 reviews in G2 ❤️. Marketing for customer-built products is hard 🥉. 12 customer stories 👻. 2 hours downtime 🥲. New navigation ⛵️. My Space 🔒.

Imagine a candidate who is considering two or more similar startups. Guess what might convince them to go with Fibery? Progress. Or, more exactly, an understanding that Fibery is persistently making progress and, therefore, has a decent chance to become successful. Delivered through these very updates.

Last year, Michael (Fibery’s CEO) started writing year-in-review posts too. I didn’t mention them because there’s just one post for now. You can read his 2021 review here.

Open Startup page with metrics

The second tool that Fibery employs to share their progress is the /open-startup page. Like monthly updates, it gives candidates a good idea of how the business is doing. This understanding, however, comes from a different source: pure numbers. And numbers often speak louder than words.

A screenshot of Fibery's Open startup page

How PostHog communicates their progress

Story page in the handbook

In the PostHog’s handbook, they have a page called Story. It succinctly shows the milestones the company has hit so far. For each milestone, they offer a clear and concise explanation of what happened, sometimes no longer than a sentence. As a result, candidates can get a good idea of how things are going in less than a minute. That’s something to aspire to.

Here’s the section titles:

  • Jan 2020: The start
  • Feb 2020: Launch
  • Apr 2020: $3M Seed round
  • May 2020: First 1,000 users
  • Oct 2020: Billions of events supported
  • Nov 2020: Building a platform
  • Dec 2020: $9M Series A
  • Jun 2021: $15M Series B
  • Sep 2021: Product Market fit achieved for PostHog Scale

A screenshot of PostHog's Story page

How we at Wasp communicate our progress

Blog posts covering big milestones (YC, $1.5m seed)

For each milestone, Matija and Martin (Wasp founders) write a blog post describing not only what they accomplished but also how they did it.

For example, when Wasp got into YC, they didn’t just post the news on Twitter. They wrote a blog about their journey to Y Combinator. It got thousands of views.

Same with fundraising. When Wasp closed a $1.5m seed, Matija documented and shared their fundraising learnings in a blog post. It ended up on the HN frontpage. (Incidentally, this post communicates something important about the founders. It takes persistence to run 250+ meetings in 98 days.)

A screenshot of Wasp's fundraising learnings blog post

Monthly newsletter with updates

To keep the momentum, Matija also writes a monthly newsletter. It’s similar to Michael’s Startup Diary in substance, but has a different style. Wasp style. (Which, again, communicates our culture.)

Like PostHog’s Story page, Wasp’s monthly updates give candidates a bird’s eye view over everything that’s happened in the past two years. To anyone interested in connecting the dots, this page is a gem.

A screenshot of Wasp's monthly newsletter archives

So, why should people join your startup?

The founders are interesting / fun / smart / human / you name it

The team is great

The culture is amazing

The business is doing well

By communicating all these reasons well, what Canny, Fibery, PostHog, and (we hope!) Wasp really end up transmitting is two powerful messages:

  • The company is likely to succeed
  • Working there will be awesome

These two messages are the real answer to “why people should join your company.” The trick, however, and the reason why I wrote this post, is that you can only transmit them indirectly. You can’t say “our founders are great.” You need to provide candidates with many-many facts about the founders, which their minds will then fuse into this abstract conclusion. Ditto for expertise, team, culture, and progress. Eventually, these first-level abstractions will blend into still broader ones: “the company is likely to succeed” and “working there will be awesome.”

Thus, there’s no single, ultimate answer to “why people should join your company.” There’s only a complex system of concrete, specific units of information from which candidates make the answer themselves. In other words, you can’t teach them why your company is likely to succeed and why working here will be awesome. But you can outline the facts and let them learn for themselves. I hope this post shows how to do that outlining well, and I hope you will apply this knowledge to bring talented people onboard and build great things.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to communicate why your startup is worth joining

· 31 min read
Vasili Shynkarenka

Except for a handful of companies who send people to Mars or develop AGI, most startups don’t seem to offer a good reason to join them. You go to their websites and all you see is vague, baseless, overly generic mission-schmission/values-schvalues HR nonsense that supposedly should turn you into a raving fan of whatever they’re doing and make you hit that “Join” button until their servers crash. Well…

Some people think that’s because most startups aren’t worth joining. I disagree. This argument generalizes one’s own reasons for joining a startup onto every other human being out there, which is unlikely to be true. I think most startups, no matter how ordinary, do have a reason to join them; a good reason; even many good reasons — they just fail to communicate them well. They’re like a shy nerd on Tinder with an empty bio and no profile pic: a kind, intelligent, and thoughtful human being who, unfortunately, will be ruthlessly swiped left — not because he’s a bad match but because his profile doesn’t show why he’s a good one.

Visually, this “Tinder profile problem” looks like this:

Illustration of candidates not seeing why to join a startup

Now, look what would happen if a startup communicated a bit better. Suddenly, our candidates could see a reason to join. If the reason is good, they might even swipe right.

Illustration of candidates seeing one reason to join a startup

But most startups have many good reasons to join them. If only they communicated well, the outcome would be something like this:

Illustration of candidates seeing many reasons to join; one candidate already running for it

Now, you’re probably wondering just what exactly those reasons are.

Here’s a rough list:

  1. The founders are interesting / fun / smart / human / you name it
  2. The team is great
  3. The culture is amazing
  4. The business is doing well

However, if you just copy this list and paste it on your jobs page, you will accomplish nothing. The candidates will never believe you. What you need to do instead is to supply them with a system of concretes (facts) from which their minds will form these abstract conclusions.

For example:

  • Instead of declaring that “the founders are reflective, thoughtful, and persistent,” show them how so, like Sarah from Canny does by writing comprehensive year-in-review blog posts for four years in a row.
  • Instead of proclaiming that “the founders are humble and can have fun,” show them how so, like Michael from Fibery did by becoming a hero of this hilarious page. (No businessy founder would ever agree to make this public. Michael did.)
  • Instead of purporting that “the team is great” or “you’ll work alongside very smart people” (God, I hate that one!), show them who exactly those people are, as PostHog does here and Wasp does here and here.

In the rest of the post, I’ll go through the four broad reasons to join a startup one by one and show real-life examples of communicating them well. In the end, I will explain how these four reasons, communicated well, fuse into two compelling messages that will interest any candidate.

One last thing. For the sake of clarity and comprehension, I will write in the second person. Instead of saying “candidates would never believe them,” I will say “you would never believe them.” It’s much easier to read and understand.

Possible reasons why your startup is worth joining, and how to communicate them well

1. FOUNDERS — or, the founders are interesting / fun / smart / human / you name it

Most startups have curious, interesting, ambitious, terribly smart founders; the kind most of us would love to work for if we had a chance. Sadly, only a few leverage this asset. In most cases, all you get is a small round pic with a fancy title and a few abstract, high-level sentences that cause no excitement whatsoever. What a shame!

How Canny commmunicates who their founders are

Founder Stories blog category

The first notable thing Canny does is the Founder Stories category in their blog. By quickly skimming the posts, you can understand that Sarah and Andrew (the founders):

If they just pinned this list of virtues to their Jobs page, you would never believe them. Instead, Sarah and Andrew show what actions they take, how they work, how they think, how they live — and you make up their own mind about what kind of people Sarah and Andrew are from seeing all that. The difference is enormous.

Note their writing style. They don’t claim to be know-it-alls with titles like “How to bootstrap your startup.” Instead, they write “How we Bootstrapped our SaaS Startup to Ramen Profitability.” They cover only what they know instead of overgeneralizing. This shows both expertise and humility.

A screenshot of Canny's Founder Stories blog category

Personal Instagram

The second thing Sarah and Andrew do well to communicate who they are is their Instagram. They don’t post glamorous keynote appearances, as many entrepreneurs do. They share the actual day-to-day working life — both the fun and the struggle. It gives you a good idea of what they’re after in life. (Not keynotes.) That’s why it works, and that’s why people love it.

A photo from Sarah and Andrew's personal Instagram

Side note: Sarah explains how she develops the Canny brand in this post. If you want to build a good one, give it a read. She also wrote about how they attract top talent. You can read it here.

How Fibery communicates who their founder is

Startup Diary blog post series

While you can get a pretty good idea of Michael (the founder) from the hilarious “Remote” page Fibery shipped last year, his Startup Diary post series offers an even better insight into his soul. In these monthly posts, Michael honestly shares everything that’s going on with Fibery, including the good, the bad, and the ugly: firing people for poor performance, losing important customers, and failing to reach product-market fit. The fact that he’s already written 45 of those (as of Aug 2022) is also telling. And he’s not a native English speaker. If he can do that, why can’t you?

A screenshot of Fibery's Startup Diary blog category

Crazy challenges

Besides writing the Startup Diary, Michael also embarks on crazy challenges like writing 100 posts about products. Only a passionate, driven person would commit to such a thing. You cannot help but respect him for it. (Before this challenge, he wrote 100 Medium posts in 100 days in 2018. You can read them here. Just scroll a few screens to reach the old stuff.)

A screenshot of Fibery's 100 posts about products blog category

If you look carefully, you’ll notice that Michael’s thinking about building a company is different from Sarah’s. For example, he despises the gentle, soothing “Oh don’t worry that it didn’t work out; you did such a good work!” approach, which is ubiquitous in the modern startup world. Instead, he states that dissatisfaction leads to progress, referring to the famous “Not quite my tempo” scene from Whiplash. Does that make you like him more than Sarah?

It depends. If you believe that being soft and balanced is better, you’ll go with Sarah; if you believe that real progress comes only from working yourself to the bone, you’ll go with Michael (or Elon). The important thing is that both founders have their own, unique viewpoints of how things should be done, and that they communicate these viewpoints as-is instead of chopping their legs off to fit the latest Procrustean fad.

In-depth, original blog posts about the industry

Some entrepreneurs say that doing a startup is like “jumping off a cliff and building your wings on the way down.” Some of it might be true. But if you want reasonable people to jump with you, you better tell them that you have a degree in engineering and know how to assemble wings in a free fall. Otherwise, the only team you’ll recruit is a suicide squad looking for a splashy hit.

To communicate his expertise, Michael writes in-depth, original, theoretical posts about the nature of knowledge management and organizational productivity. These posts are gems, both literally and metaphorically. (They’re filed under the Gems category in the Fibery blog.)

For example:

After reading these articles, you understand not only that Michael really knows how to build wings while falling off the cliff, but that he has already jumped a few times. (Prior to Fibery, Michael had worked on knowledge management for more than a decade. He also had built a successful project management software, Targetprocess.) You know that he’s an expert who can be trusted.

Interestingly, even though Michael writes differently from Sarah, they both leverage what they’re good at. Sarah does not try to produce treatises on software development philosophy, and Michael doesn’t gush out with his personal learnings from building a startup. That, I think, is the right way to do it.

How PostHog communicates who their founders are

PostHog’s founders James and Tim don’t write 100 posts in 100 days or run a personal Instagram. But they’ve come up with something else to communicate what kind of people they are. And it’s something unique.

Well-written, concise bio

First, both founders have decent profiles in the company handbook. These bios are short, clear, and humane. They’re also very specific. Where else have you seen the name of the CEO’s cat?

A screenshot of James Hawkins' bio in the PostHog Handbook

Personal README files

Second, both James and Tim have an extensive README file (one, two) on how to work with them. These files give you an insight into their productivity habits, interests, and quirks. In fact, after reading them, you will likely have a better idea of the founders than you’d usually get from working at a company for a month!

For instance, James’s file has sections like:

  • Short bio. Includes very specific details like: “I tend to work 9am to 5pm with an hour for lunch, then I have a gap to have dinner with my family, then 9pm to around 11pm ish.”
  • Very clear areas of responsibility. No need to wonder what the hell the CEO is doing anymore!
  • Quirks. These are remarkably humble and open-minded, like:
    • “If I haven’t responded to something that you’ve sent me, that’s probably because I’ve read it and don’t feel particularly strongly - so just make a call on what to do if you don’t hear back in a reasonable time frame.”
    • “I’m a little disorganized. I compensate for this by making sure the teams I work on have this skill. Often I think this actually helps me prioritize the things that really matter.”
    • Explaining these quirks is an ingenious move. Besides explaining how to work with James, this section communicates that he’s profoundly self-aware and willing to accept and leverage his weaknesses. These qualities are very rare and incredibly valuable.
  • What I value. In stark contrast to most HR nonsense, these values are very clear, very specific, and written in English rather than HRese. (I just came up with this term: it means “legalese but for HR.”) Here are two examples:
    • “Proactivity. Do not ask me for permission to do things - I wouldn’t have hired you if I didn’t trust you. I’d rather 9 things get done well and 1 thing I disagree with than we don’t get anything done at all.”
    • “Directness impresses me. If you don’t like something please just say so. It makes for much healthier relationships.”

In addition to that, there’s also: How I can help you, How you can help me, My goals until end December 2022 (very specific!), Personal strategy, Execution todo (including “1 bike ride a week”!) and Archived todo.

In summary, this README page is a gem. I wish more founders had them.

A screenshot of James Hawkins' README in the PostHog Handbook

How we at Wasp communicate who our founders are

“Who we are” section of every job description page

Matija and Martin (the founders of Wasp) embedded a concise description of who they are right into each job description page in Notion. They knew that this is the first company artifact many candidates will see. So they saved candidates time and effort on digging up who the hell started Wasp.

Note the language and substance of this list. When you read it, you immediately get a sense of who Matija and Martin are as people — fun, easygoing, no-corporate-bullshit kinda guys. Now imagine it said something “more normal,” like: “The company was founded by seasoned entrepreneurs…” What impression would that make?

A screenshot of Wasp's job description page

2. TEAM — or, the team is great

It is startling how little most startups tell you about their teams. Often all you get is a chessboard of faces and titles, which gives you no idea who these people are as people or how working with them will feel like. Given how crucial a reason “great team” is for most candidates, improving how you communicate it seems like a low-hanging fruit.

How Canny communicates who is on their team

Decent team page

The Canny’s difference starts with a team page. It has a dense summary of who each team member is as a person and includes high-quality, lively photos of everybody.

A screenshot of Canny's Team page

Look how specific those bios are. In most cases, all you get here is a generic “developer” or “marketer” without any personal details. Bios of robots, not people. No wonder nothing comes to mind, except perhaps for Agent Smith. But Canny’s bios are different. When you read them, you can actually imagine the person! They’re Neos in the world of Smiths.

Remarkable “Why work at Canny” blog post

From there, it gets only better. Canny’s chief weapon for explaining their team is a blog post, the “Why work at Canny” blog post. Sarah wrote it back in the summer of 2021. It is full of quotes from team members and photos of their workdays and vacations. Real photos of real people. No wonder the comments section under the post abounds with raving fans willing to join the team straight away!

A screenshot of comments under the Canny's Why work at Canny blog post

Perhaps the best thing about this post is how little work it takes to create one. I imagine that collecting the data took some time, but the actual writing (it’s an 11-min read) took no more than a week. A week of work for a candidate magnet of such tremendous power? Sounds like a deal.

P.s. Sarah writes a lot more about their team in her yearly review posts, but I decided not to elaborate on those for the sake of clarity. You can check them out here: year 1, year 2, year 3, and year 4.

How Fibery communicates who is on their team

Weird About Us page

Unlike Canny and PostHog’s, Fibery’s About Us page doesn’t reveal much info about each team member. You will find no bios or README files there. But it clearly tells you one thing: the team is a bunch of weirdos. So, if weird is your thing, you’ll be attracted to Fibery like a moth to a flame. (Side note: Fibery managed to clearly explain their vision in one paragraph. This is rare.)

A screenshot of Fibery's About Us page

I’ve already mentioned Michael’s Startup Diary monthly blog series. What I didn’t say is that each post communicates something about the team: who did what that month, random Slack posts (links, quotes, tweets, and images), etc. If someone new joined that month, Michael writes a few paragraphs explaining who that person is, where they come from, what they’re going to do at Fibery, and even attaches a photo. Like Chris.

A screenshot of Fibery's Startup Diary blog post

How PostHog communicates who is on their team

Team section in the company handbook

At PostHog, every team member has a well-written, few-paragraphs-long bio and a stylish illustration on the Team section of the PostHog’s Handbook. (Which is a work of art worthy of its own blog post, by the way.) Many team members have their own README files, like the founders do. Check out Lottie Coxon’s, PostHog’s Graphic Designer’s README here, and some others here and here. Even a quick read through these bios and READMEs gives you a good idea of who PostHog has on board.

A screenshot of PostHog's team section in the handbook

Another screenshot of PostHog's team section in the handbook

Day-in-life videos from employees

In addition to bios and READMEs, PostHog has a day-in-life video of Lottie, their graphic designer. It communicates a lot more information about what kind of person she is and how working at PostHog feels like than her bio. I wish they had more of those.

A screenshot from PostHog's graphic designer day-in-life video

Finally, PostHog’s handbook offers two more sections where candidates can learn even more about the team: Culture and Team structure. All are worth a read, and each tells you something new about the company and the team, nurturing your liking and respect for these people. Definitely worth stealing.

How we at Wasp communicate who is on our team

“Meet the team” blog posts

To help candidates understand who they will be working with, we at Wasp write a blog post about each new hire:

The posts are brief enough to be read in one sitting. Yet, they are very informative. Basically, each post is an interview, presented as an article. We hope they give candidates a good idea of who they'll be spending half of their waking time with.

A screenshot of Wasp's Meet the team blog post

3. CULTURE — or, the culture is amazing

While researchers still argue about the ultimate definition, most of us understand culture as “what working here feels like” and/or “how we do things here.” We also understand how crucial it is for those looking for work. It seems glaringly obvious that startups should work hard on communicating their culture. Yet, most companies don’t. Or, even worse, they flood their websites with meaningless HR fluff, which only scares interesting people away. In short, communicating culture well is another low-hanging fruit waiting to be picked.

How Canny communicates their culture

Canny does an outstanding job at communicating their culture. The primary tool they employ is, once again, their blog. (Note how multifunctional it is: founders, expertise, team, and now culture.) The posts in the Founder Stories category convey very well what working at Canny feels like. Here are a few examples.

“Why work at Canny” blog post

I’ll risk repeating myself, but this post so beautifully explains Canny’s culture that I couldn’t resist. It mentions why and how they work remotely, how they do team retreats (with photos and a video from Lisbon!), and how they had fun together playing weird Zoom games when travel was not an option due to Covid.

Pay attention to the imagery. It communicates a lot more information than any lengthy, elaborate description would. Indeed, a picture is often worth a thousand words.

A photo of Canny's two team members hacking in Denver

“Lessons from a year of team retreats” blog post

Instead of saying that “team is our priority” or “we invest in our people,” Sarah shows what they’ve done to support their team.

Again, note how specific the imagery is.

A photo from Canny's Lessons from a year of team retreats blog post

Interestingly, Sarah’s post isn’t framed as “hey we do many team retreats, we’re awesome, come work for us.” If they wrote that, the reader would feel uneasy. They would sense bragging. That’s why the explicit message in the post is what Canny learned doing team retreats, not that they’ve done many. This explicit message, however, implies that they indeed have done many retreats! It sends a message that Canny cares for their employees without explicitly saying so. This is what true mastery looks like.

“The end of our digital nomad journey” blog post

Although this post describes Sarah and Andrew’s personal nomad experience, Sarah managed to reveal Canny’s culture through it. To do that, she described how the team worked on Canny during those nomad years. She also wrote about their communication struggles, routines, and a lot more. And, again, look at how effectively her seemingly imperfect screenshots and photos transmit the vibe!

A photo from Canny's The end of our digital nomad journey blog post

Another photo from Canny's The end of our digital nomad journey blog post

How Fibery communicates their culture

While Fibery’s culture is different from Canny’s, they also communicate it well. Their primary tool is a weird, quirky website full of special projects that give you a sense of how they do things at Fibery and what working there feels like.

Anxiety page

The first project is Fibery’s /anxiety page. Launched in 2019, it mocks every serious enterprise software out there with puns like “Yet another collaboration tool” as the page title, “Mistake” as a sign-up button text, and, my favorite, “Try—Suffer—Quit” page structure.

A screenshot of Fibery's /anxiety page

One day three years ago, someone submitted this page to Hacker News. The post surged to the top of the frontpage, stayed there for many hours, and got 705 upvotes and 145 comments from people all over the world relating to Fibery’s culture. Why? Because it felt real.

Here’s a glimpse of what people wrote in the comments:

A screenshot of Hacker News comments on Fibery's /anxiety page

Another screenshot of Hacker News comments on Fibery's /anxiety page

Remote page

The second special project Fibery did to communicate their culture is the /remote page. It shows what working from home is really like. It’s the funniest thing I’ve ever seen done by a software startup. (Have you ever seen a CEO being licked by a dog?) It also shows how the Fibery team works and even how they use Fibery to build Fibery. Like Canny’s “Lessons from a year of team retreats” blog post, it does so implicitly. A true masterpiece.

Weird, humorous site

Broadly, the whole site screams that Fibery is a place for misfits, rebels, and trouble makers; the place where such people will be valued and will feel like home; the place built around brutal honesty and spicy humor.

The “What (non-)customers say” section is worth a mention. Over my nine years in startups, I haven’t seen a site that a) lists bad customer reviews; and b) uses 💩 emoji as a filter. Again, this is telling. It says a lot about who they are as people: humble, real, and fond of humor.

A screenshot of Fibery's About Us page, What non-customers say section

How PostHog communicates their culture

Comprehensive company handbook covering all-things culture

PostHog’s way of communicating their culture is the most explicit of all four examples, yet very effective. Their primary tool is the PostHog Handbook, which covers virtually every aspect of what working at PostHog feels like: interviews, onboarding, training, management, communication, and even firing. (They call it offboarding.)

The handbook goes all the way up to the high-level strategy, which is very clear. Notably, PostHog’s strategy section not only puts forth ambitious goals but actually explains how exactly the company will get there.

The values section is very specific; perhaps the most specific I’ve ever seen. PostHog does not merely list their values as meaningless abstractions but supports them with evidence. Some values have many paragraphs of examples demonstrating how the team follows them.

A screenshot of the Values section in the PostHog's handbook

They also have a specific Culture page with a 5-minute video from the CEO explaining how they designed PostHog for remote work from day one, which nicely complements the text.

A screenshot from James Hawkins's video

In summary, if Canny’s weapon of choice is the blog and Fibery’s is the website, then PostHog’s is definitely the handbook. It’s a work of art.

How we at Wasp communicate our culture

Easygoing vibe from memes, copy, and imagery

Unlike Posthog, we at Wasp don’t (yet) have a dedicated Culture page. We are too small for that. But that doesn’t stop us from showing what working at Wasp feels like. We just use different tools.

Our Twitter, blog, and monthly updates abound with memes, GIFs, and hilarious imagery. Plus, we write them in a humorous, lighthearted, easygoing style. By just scrolling through these things for a few minutes, candidates can understand that we aren’t some corporate bros. And if they like working on interesting things while having fun, they won’t help but feel an inkling to reach out.

A funny image from Wasp's blog post about GitHub Copilot

A photo of Wasp's team packing t-shirts for users

4. PROGRESS — or, the business is doing well

When you just closed an $80 million Series B or signed Facebook as a customer, communicating progress is easy. You just state these facts. However, most companies need to attract great people way before Series B. In fact, it is these very people who’re going to get you there. As most startups are secretive about how things are going, communicating that things are going somehow — no matter how negligible your progress in contrast to the big guys — becomes quite an advantage. It immediately de-risks the opportunity in the candidate’s eyes. So, if EXPERTISE is about convincing candidates that you know how to build the wings, PROGRESS is about showing them the half-built carcass on your way down. Both are important if you want great people to jump off the cliff with you.

How Canny communicates their progress

To give candidates a sense that things are moving, that this company is not some long slog but a place where progress is made every day, that they can become a part of something that’s growing and, therefore, can grow themselves, to do all that, Canny does two things.

“Year in review” blog posts

The first one is their “Year in review” blog post series. Such comprehensive, thoughtful reviews are rare in the startup world. What is even rarer is when these posts span over four consecutive years. It sends a message that the founders are persistent and devoted to making this company successful.

Below are all Canny’s year-in-review posts in a sequential order:

A screenshot of Canny's Year in review blog post

Important revenue milestones blog posts

In addition to year-in-review posts, Sarah writes about hitting notable revenue milestones. Like with yearly reviews, such transparency is rare. It attracts attention, causes liking, and builds trust.

For example:

A screenshot of Canny's How we built a $1m ARR SaaS startup blog post

Short tweets with progress summary

Finally, Sarah occasionally tweets short summaries of their progress, like this one. These tweets work like ads. Over time, a candidate’s brain fuses them into a broader idea like “Canny is growing” or “Canny is doing well.” Then, once a candidate decides to change jobs, it nudges the candidate to consider Canny.

A screenshot of Sarah’s tweet with progress update

How Fibery communicates their progress

Startup Diary blog posts

The most notable thing Fibery does to communicate their progress is the Startup Diary blog posts series written by the founder, Michael, every month, for the past 45 months. It’s the longest series of monthly updates I know. In these posts, Michael honestly shares everything that’s going on with the company: the good, the bad, and the ugly.

Below are just a few examples, selected by me. You can study all Fibery’s monthly updates here.

  • #2 Slow September 2018 — Fibery startup progress in September 2018. Slow month with not so many news. First positive feedback. Company name selection.
  • #6 Planning Private Beta in January 2019 — Fibery startup progress in January 2019: Private beta goals, selecting a market positioning (hard), apps re-design.
  • #10 Burn in May 2019 — Several people burned out, new features are delivered, public release will be sooner (we hope) (despite ill fortune).
  • #16 Crazy November 2019 — Fibery 1.0 is silently launched. Silence is hard to keep. HackerNews front page. Twitter madness. 3000 registered accounts.
  • #17 Fragmented December 2019 — Public announcements moved to January. +Lena. Tons of feedback. First money! Hype is over. We consider rising a ~$4M round.
  • #35 Raised $3.1M in July 2021 — TLDR: We closed $3.1M seed round. Building a second brain for teams. Fibery mission. Building in Public. Automation rules. Documents and Rich Text history.
  • #36 20k MRR in August 2021 — Special Startup Diary edition. 20k MRR & 15 new customers! +Chris. +Sales agency. 4 case studies. Airtable integration & notify people action.
  • ($30K MRR) #42 Connecting the dots in April 2022 — TLDR: 🇺🇦 Ukrainian war affected our performance. $30K MRR 🐌. 69 reviews in G2 ❤️. Marketing for customer-built products is hard 🥉. 12 customer stories 👻. 2 hours downtime 🥲. New navigation ⛵️. My Space 🔒.

Imagine a candidate who is considering two or more similar startups. Guess what might convince them to go with Fibery? Progress. Or, more exactly, an understanding that Fibery is persistently making progress and, therefore, has a decent chance to become successful. Delivered through these very updates.

Last year, Michael (Fibery’s CEO) started writing year-in-review posts too. I didn’t mention them because there’s just one post for now. You can read his 2021 review here.

Open Startup page with metrics

The second tool that Fibery employs to share their progress is the /open-startup page. Like monthly updates, it gives candidates a good idea of how the business is doing. This understanding, however, comes from a different source: pure numbers. And numbers often speak louder than words.

A screenshot of Fibery's Open startup page

How PostHog communicates their progress

Story page in the handbook

In the PostHog’s handbook, they have a page called Story. It succinctly shows the milestones the company has hit so far. For each milestone, they offer a clear and concise explanation of what happened, sometimes no longer than a sentence. As a result, candidates can get a good idea of how things are going in less than a minute. That’s something to aspire to.

Here’s the section titles:

  • Jan 2020: The start
  • Feb 2020: Launch
  • Apr 2020: $3M Seed round
  • May 2020: First 1,000 users
  • Oct 2020: Billions of events supported
  • Nov 2020: Building a platform
  • Dec 2020: $9M Series A
  • Jun 2021: $15M Series B
  • Sep 2021: Product Market fit achieved for PostHog Scale

A screenshot of PostHog's Story page

How we at Wasp communicate our progress

Blog posts covering big milestones (YC, $1.5m seed)

For each milestone, Matija and Martin (Wasp founders) write a blog post describing not only what they accomplished but also how they did it.

For example, when Wasp got into YC, they didn’t just post the news on Twitter. They wrote a blog about their journey to Y Combinator. It got thousands of views.

Same with fundraising. When Wasp closed a $1.5m seed, Matija documented and shared their fundraising learnings in a blog post. It ended up on the HN frontpage. (Incidentally, this post communicates something important about the founders. It takes persistence to run 250+ meetings in 98 days.)

A screenshot of Wasp's fundraising learnings blog post

Monthly newsletter with updates

To keep the momentum, Matija also writes a monthly newsletter. It’s similar to Michael’s Startup Diary in substance, but has a different style. Wasp style. (Which, again, communicates our culture.)

Like PostHog’s Story page, Wasp’s monthly updates give candidates a bird’s eye view over everything that’s happened in the past two years. To anyone interested in connecting the dots, this page is a gem.

A screenshot of Wasp's monthly newsletter archives

So, why should people join your startup?

The founders are interesting / fun / smart / human / you name it

The team is great

The culture is amazing

The business is doing well

By communicating all these reasons well, what Canny, Fibery, PostHog, and (we hope!) Wasp really end up transmitting is two powerful messages:

  • The company is likely to succeed
  • Working there will be awesome

These two messages are the real answer to “why people should join your company.” The trick, however, and the reason why I wrote this post, is that you can only transmit them indirectly. You can’t say “our founders are great.” You need to provide candidates with many-many facts about the founders, which their minds will then fuse into this abstract conclusion. Ditto for expertise, team, culture, and progress. Eventually, these first-level abstractions will blend into still broader ones: “the company is likely to succeed” and “working there will be awesome.”

Thus, there’s no single, ultimate answer to “why people should join your company.” There’s only a complex system of concrete, specific units of information from which candidates make the answer themselves. In other words, you can’t teach them why your company is likely to succeed and why working here will be awesome. But you can outline the facts and let them learn for themselves. I hope this post shows how to do that outlining well, and I hope you will apply this knowledge to bring talented people onboard and build great things.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html b/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html index e3465a46d6..2287cf01bf 100644 --- a/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html +++ b/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html @@ -19,13 +19,13 @@ - - + +
-

How and why I got started with Haskell

· 8 min read
Shayne Czyzewski

I have been programming professionally for over a decade, using a variety of languages day-to-day including Ada, C, Java, Ruby, Elixir, and JavaScript. I’ve also tried some obscure ones, albeit less frequently and for different purposes: MIPS assembly language and OCaml for academic work (I’m a BS, MS, and PhD dropout in CS), and Zig for some side projects. In short, I like learning new languages (at least at a surface level) and have been exposed to different programming paradigms, including functional.

Yet, I have never done Haskell. I’ve wanted to learn it since my college days, but never got the time. In late 2021, though, my curiosity took over. I wanted to see for myself if the mystique and the Kool-Aid hype (or hate) around it are justified. :P So, I decided I’d start learning it on the side and also look for a company that uses it as my next gig. That’s how my Haskell journey started, and how I got into Wasp a few months later.

Why learn Haskell?

Haskell seems to have an aura of superiority around it. Many niche and heavily academically-inspired languages do. These languages seem to be used by the enlightened minds and allow you to quickly write complex programs in a fraction of the time with significantly less code. Lisp is amongst these languages, too. Yet, nobody uses them for anything real — only toy projects. (While stroking their long, grey beards under a tree, ruminating on the philosophy of computer science.) At least, that’s the impression I got in college and at work. So, what makes Haskell interesting to learn, let alone want to use professionally?

First, it is functional as it gets. While I have used lambdas and functional concepts like map in non-functional languages, the fact that these were my only choice was really interesting to me. After years of extensive OO usage, I’ve come to appreciate this epigram by Alan Perlis. I think it captures a mindset shift between the two paradigms:

“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.” — Alan Perlis

In OO, you create lots of classes with lots of methods. In functional, you have far fewer data structures (mostly list) with a lot more functions. So basically more functions to operate on fewer nouns, whereas OO is lots of nouns, each with many bespoke methods. (The first comment on this Stack Overflow thread explains it really well.)

Besides, I liked the idea of referential transparency when writing pure functions. It means that you get the same result back every time you invoke a function, without fear of unknown side effects. (But the language does offer the flexibility to have side effects like IO, via Monads.) I also liked having only immutable data structures — they make reasoning about the system and data flow easier. There were many things like these two that I liked. The point is that thinking functionally really changes the way you structure and solve problems, so I was curious to give it a go.

Second, Haskell is lazy. While there are pros and cons to this, it feels undeniably different. Most languages are strict, in that all function arguments are evaluated before invoking a function. This is required because of side effects; to have some expectations regarding the order in which things will run. Haskell does the opposite: it delays evaluation until it’s actually needed.

One contrived yet helpful example of laziness is infinite data structures. Below, we define fibs as an infinite List of Integer values, by using references to itself! (You can find a runnable example here.)

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]

There’s a downside to laziness, too. It makes it harder to reason about performance and resource utilization. But the idea that you can define things in a declarative way but know that they are evaluated only when needed is a pretty eye-opening way to program.

To sum up: Haskell is functional, lazy, and strongly statically typed. Just the trifecta that gets me out of bed in the morning! :D So, how did I go about learning it?

Hello Haskell!

I started by reading the canonical Haskell newbie resource, “Learn You a Haskell for Great Good!,” often abbreviated LYAH. It was very entertaining, and I learned a lot from it. At times, I wanted it to get to the point more quickly. Still, despite the amusing images and often lengthy examples, it provided me with a great conceptual foundation. I highly recommend it as your first read — it is a really well-written resource for beginners.

After I was about 80% done with LYAH, I switched to a more recent but still popular book: “Haskell Programming from First Principles.” I liked that it started with fundamentals and then moved to more complex topics, slowly but steadily developing my understanding. It was pretty long, though, and sometimes went too far into the weeds. It also had a tinge of intellectual flexing at certain points. Still, it was a good read. I’d read it again if I were starting over.

I also tried a Haskell course from Google. Despite being brief, it explains the key concepts in a relatively complete way. If videos are your thing, it might be a solid way to get up to speed.

In short, skimming an intro book to get your foundation solid would be the best bet. I’d also recommend trying out many different online resources when covering more intermediate topics, like Monad Transformers, for example. And don’t worry if it takes a while to start feeling comfortable with things that are pretty specific to Haskell! It just takes some time, and often it is more confusing to derive/deeply understand than to just start using them at first. The understanding will come over time. (Of course, sometimes pictures help!)

Setup and IDE support

Getting Haskell up and running was surprisingly straightforward, even though I ran it on an M1 MacBook Air, which was considered a pretty new architecture in 2021. Since the entire toolchain was not fully ARM-compatible back then, some of the setup advice required a bit of modification. But that was no big deal: I used ghcup, installed HLS in VS Code, and bam! — I had Haskell up and running. It was a pretty nice experience.

Some minor downsides I recall:

  • There doesn’t seem to be a consensus on which build and package management tool to use, Cabal or Stack. However, unless you’re doing something super specific, it’s not an irreversible decision. At Wasp, we started with Stack but then migrated to Cabal since it better fit our setup and workflows. It was pretty seamless.
  • One thing I do miss from other IDEs is breakpoint debugging. Technically, there’s some support for it in Haskell, but I don’t think many use it. Breakpoints and lazy evaluation don’t seem to be BFFs.

0-60 at work

For someone with experience in several different languages, it is pretty achievable to be able to solve minor bugs/features in Haskell after a few weeks of learning. At least, it was for me. I certainly struggled on best practices and such, and my code reviews involved some Haskell golfing comments for sure :) But I could make it do what I wanted it to do from the functionality perspective. Kudos to the mostly helpful compiler errors (with a bit of practice reading) and the Internet!

Hopefully, your code base demonstrates established project and Haskell patterns, so you can learn as you poke around, and your early code reviewers are supportive coworkers who can explain things as part of their suggestions. I was quite fortunate in that regard: the Wasp team values teaching and learning, and the codebase uses what is called “Simple Haskell”, which limits the use of excessive language extensions in the hopes to keep the core language and concepts as tight as possible. (Note: there are Haskell experts who view this as a severe limitation of the capabilities of the language, but as a newbie, I was happy they did it.)

So, was the juice worth the squeeze?

Learning Haskell took considerable time and effort. It was completely different from any language I had used before. Yet, I am very happy I embarked on this journey. Even if you do not intend to get a job using Haskell, I still think learning it is worthwhile just to expand your programming point of view and master functional concepts. And for a select set of project types (like writing a compiler for a full-stack web DSL), I feel it really will make you more productive over time. Give an intro to Haskell tutorial or video a try some weekend and let me know what you think! I’m at shayne at wasp-lang dot dev dot com.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How and why I got started with Haskell

· 8 min read
Shayne Czyzewski

I have been programming professionally for over a decade, using a variety of languages day-to-day including Ada, C, Java, Ruby, Elixir, and JavaScript. I’ve also tried some obscure ones, albeit less frequently and for different purposes: MIPS assembly language and OCaml for academic work (I’m a BS, MS, and PhD dropout in CS), and Zig for some side projects. In short, I like learning new languages (at least at a surface level) and have been exposed to different programming paradigms, including functional.

Yet, I have never done Haskell. I’ve wanted to learn it since my college days, but never got the time. In late 2021, though, my curiosity took over. I wanted to see for myself if the mystique and the Kool-Aid hype (or hate) around it are justified. :P So, I decided I’d start learning it on the side and also look for a company that uses it as my next gig. That’s how my Haskell journey started, and how I got into Wasp a few months later.

Why learn Haskell?

Haskell seems to have an aura of superiority around it. Many niche and heavily academically-inspired languages do. These languages seem to be used by the enlightened minds and allow you to quickly write complex programs in a fraction of the time with significantly less code. Lisp is amongst these languages, too. Yet, nobody uses them for anything real — only toy projects. (While stroking their long, grey beards under a tree, ruminating on the philosophy of computer science.) At least, that’s the impression I got in college and at work. So, what makes Haskell interesting to learn, let alone want to use professionally?

First, it is functional as it gets. While I have used lambdas and functional concepts like map in non-functional languages, the fact that these were my only choice was really interesting to me. After years of extensive OO usage, I’ve come to appreciate this epigram by Alan Perlis. I think it captures a mindset shift between the two paradigms:

“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.” — Alan Perlis

In OO, you create lots of classes with lots of methods. In functional, you have far fewer data structures (mostly list) with a lot more functions. So basically more functions to operate on fewer nouns, whereas OO is lots of nouns, each with many bespoke methods. (The first comment on this Stack Overflow thread explains it really well.)

Besides, I liked the idea of referential transparency when writing pure functions. It means that you get the same result back every time you invoke a function, without fear of unknown side effects. (But the language does offer the flexibility to have side effects like IO, via Monads.) I also liked having only immutable data structures — they make reasoning about the system and data flow easier. There were many things like these two that I liked. The point is that thinking functionally really changes the way you structure and solve problems, so I was curious to give it a go.

Second, Haskell is lazy. While there are pros and cons to this, it feels undeniably different. Most languages are strict, in that all function arguments are evaluated before invoking a function. This is required because of side effects; to have some expectations regarding the order in which things will run. Haskell does the opposite: it delays evaluation until it’s actually needed.

One contrived yet helpful example of laziness is infinite data structures. Below, we define fibs as an infinite List of Integer values, by using references to itself! (You can find a runnable example here.)

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]

There’s a downside to laziness, too. It makes it harder to reason about performance and resource utilization. But the idea that you can define things in a declarative way but know that they are evaluated only when needed is a pretty eye-opening way to program.

To sum up: Haskell is functional, lazy, and strongly statically typed. Just the trifecta that gets me out of bed in the morning! :D So, how did I go about learning it?

Hello Haskell!

I started by reading the canonical Haskell newbie resource, “Learn You a Haskell for Great Good!,” often abbreviated LYAH. It was very entertaining, and I learned a lot from it. At times, I wanted it to get to the point more quickly. Still, despite the amusing images and often lengthy examples, it provided me with a great conceptual foundation. I highly recommend it as your first read — it is a really well-written resource for beginners.

After I was about 80% done with LYAH, I switched to a more recent but still popular book: “Haskell Programming from First Principles.” I liked that it started with fundamentals and then moved to more complex topics, slowly but steadily developing my understanding. It was pretty long, though, and sometimes went too far into the weeds. It also had a tinge of intellectual flexing at certain points. Still, it was a good read. I’d read it again if I were starting over.

I also tried a Haskell course from Google. Despite being brief, it explains the key concepts in a relatively complete way. If videos are your thing, it might be a solid way to get up to speed.

In short, skimming an intro book to get your foundation solid would be the best bet. I’d also recommend trying out many different online resources when covering more intermediate topics, like Monad Transformers, for example. And don’t worry if it takes a while to start feeling comfortable with things that are pretty specific to Haskell! It just takes some time, and often it is more confusing to derive/deeply understand than to just start using them at first. The understanding will come over time. (Of course, sometimes pictures help!)

Setup and IDE support

Getting Haskell up and running was surprisingly straightforward, even though I ran it on an M1 MacBook Air, which was considered a pretty new architecture in 2021. Since the entire toolchain was not fully ARM-compatible back then, some of the setup advice required a bit of modification. But that was no big deal: I used ghcup, installed HLS in VS Code, and bam! — I had Haskell up and running. It was a pretty nice experience.

Some minor downsides I recall:

  • There doesn’t seem to be a consensus on which build and package management tool to use, Cabal or Stack. However, unless you’re doing something super specific, it’s not an irreversible decision. At Wasp, we started with Stack but then migrated to Cabal since it better fit our setup and workflows. It was pretty seamless.
  • One thing I do miss from other IDEs is breakpoint debugging. Technically, there’s some support for it in Haskell, but I don’t think many use it. Breakpoints and lazy evaluation don’t seem to be BFFs.

0-60 at work

For someone with experience in several different languages, it is pretty achievable to be able to solve minor bugs/features in Haskell after a few weeks of learning. At least, it was for me. I certainly struggled on best practices and such, and my code reviews involved some Haskell golfing comments for sure :) But I could make it do what I wanted it to do from the functionality perspective. Kudos to the mostly helpful compiler errors (with a bit of practice reading) and the Internet!

Hopefully, your code base demonstrates established project and Haskell patterns, so you can learn as you poke around, and your early code reviewers are supportive coworkers who can explain things as part of their suggestions. I was quite fortunate in that regard: the Wasp team values teaching and learning, and the codebase uses what is called “Simple Haskell”, which limits the use of excessive language extensions in the hopes to keep the core language and concepts as tight as possible. (Note: there are Haskell experts who view this as a severe limitation of the capabilities of the language, but as a newbie, I was happy they did it.)

So, was the juice worth the squeeze?

Learning Haskell took considerable time and effort. It was completely different from any language I had used before. Yet, I am very happy I embarked on this journey. Even if you do not intend to get a job using Haskell, I still think learning it is worthwhile just to expand your programming point of view and master functional concepts. And for a select set of project types (like writing a compiler for a full-stack web DSL), I feel it really will make you more productive over time. Give an intro to Haskell tutorial or video a try some weekend and let me know what you think! I’m at shayne at wasp-lang dot dev dot com.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html b/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html index d9ef31824d..37b906499d 100644 --- a/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html +++ b/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html @@ -19,13 +19,13 @@ - - + +
-

How to get started with Haskell in 2022 (the straightforward way)

· 7 min read
Martin Sosic

Haskell is a unique and beautiful language that is worth learning, if for nothing else, then just for the concepts it introduces and their potential to expand your view on programming.

I have been programming in Haskell on and off since 2011 and professionally for the past 2 years, building a compiler. While in that time Haskell has become much more beginner-friendly, I keep seeing beginners who are overwhelmed by numerous popular options for build tools, installers, introductory educational resources, and similar. Haskell’s homepage getting a call from the previous decade to give them their UX back :D also doesn’t help!

That is why I decided to write this opinionated and practical post that will tell you exactly how to get started with Haskell in 2022 in the most standard / common way. Instead of worrying about decisions that you are not equipped to make at the moment (e.g. “what is the best build tool?”), you can focus on enjoying learning Haskell :)!

TLDR / Super opinionated summary

  1. For setup, use GHCup. Install GHC, HLS, and cabal.
  2. As a build tool, use cabal.
  3. For editor, use VS Code with Haskell extension. Or, use emacs/vim/....
  4. Join r/haskell. Feel free to ask for help!
  5. To learn the basics of Haskell, read the LYAH book and build a blog generator in Haskell. Focus on getting through stuff instead of understanding everything fully; you will come back to it later again.

1. Setup: Use GHCup for seamless installation

GHCup is a universal installer for Haskell. It will install everything you need to program in Haskell and will help you manage those installations in the future (update, switch versions, and similar). It is simple to use and works the same way on Linux, macOS, and Windows. It gives you a single central place/method to take care of your Haskell installation so that you don’t have to deal with OS-specific issues.

To install it, follow instructions at GHCup. Then, use it to install the Haskell Toolchain (aka stuff that you need to program in Haskell).

Haskell Toolchain consists of:

  1. GHC -> Haskell compiler
  2. HLS -> Haskell Language Server -> your code editor will use this to provide you with a great experience while editing Haskell code
  3. cabal -> Haskell build tool -> you will use this to organize your Haskell projects, build them, run them, define dependencies, etc.
  4. Stack -> cabal alternative, which you won’t need for now since we’ll go with cabal as our build tool of choice

2. Build tool: Use cabal

There are two popular build tools for Haskell: cabal and Stack. Both are widely used and have their pros and cons. So, one of the hard choices beginners often face is which one to use.

Some time ago, cabal was somewhat hard to use (complex, “dependency hell”). That’s why Stack was created: a user-friendly build tool that solves some of the common issues of cabal. (Interestingly, Stack uses cabal’s core library as its backend!) However, as Stack was being developed, cabal advanced, too. Many of its issues have been solved, making it a viable choice for beginners.

In 2022, I recommend cabal to beginners. I find it a bit easier to understand when starting out (no resolvers), it works well out of the box with GHCup and the rest of the ecosystem, and it seems to be better maintained lately.

3. Editor: VS Code is a safe bet

HLS (Haskell Language Server) brings all the cool IDE features to your editor. So, as long as your editor has a decent Haskell language extension that utilizes HLS, you are good.

The safest bet is to go with Visual Studio Code — it has a great Haskell extension that usually works out of the box. A lot of Haskell programmers also use Emacs and Vim. I can confirm they also have good support for Haskell.

4. Community: r/haskell and more

Haskell community is a great place to ask for help and learn about new developments in the ecosystem. I prefer r/haskell -> it tracks all the newest events and no question goes unanswered. There is also Haskell Discourse, where a lot of discussions happen, including the more official ones. A lot of Haskellers are still active on IRC, but I find it too complex and outdated to use.

Check https://www.haskell.org/community for a full list of Haskell communities.

5. Learning: You don’t need a math degree, just grab a book

There is a common myth going around that you need a special knowledge of math (PhD in category theory!) to be able to program in Haskell properly. From my experience, this is as far from the truth as it can be. It is certainly not needed, and I seriously doubt it helps even if you have it. Maybe for some very advanced Haskell stuff, but certainly not for junior/intermediate level.

Instead, learning Haskell is the same as learning other languages -> you need a healthy mix of theory and practice. The main difference is that there will be more unusual/new concepts than you are used to, which will require some additional effort. But these new concepts are also what makes learning Haskell so fun!

I recommend starting with a book for beginners, LYAH. It has an online version that you can read for free, or you can buy a printed version if you like physical books.

If you don't like LYAH, consider other popular books for beginners (none of them are free though):

  1. Haskell Programming from first principles
  2. Get Programming with Haskell
  3. Programming in Haskell

Whatever book you go with, don’t get stuck for too long on concepts that are confusing to you, especially towards the end of the book. Some concepts will just need time to click; don’t expect to grasp it all on the first try. Whatever you do grasp from the first read will likely be more than enough to get going with your first projects in Haskell. You can always come back to those complex concepts later and understand them better. Also, don’t be shy to ask the community -> there are many Haskellers out there happy to support you in your learning!

note

When I say "don't get stuck", I don't mean you should skip the difficult concept after the first hurdle. No, you should spend some hours experimenting, looking at it from different angles, playing with it, trying to crack it. But you shouldn't spend days trying to understand the same concept (e.g. function as a monad) and then feel defeated due to not grasping it 100%. Instead, if you put proper effort but stuff is not completely clicking, tap yourself on the back and move on for now.

Once you take the first pass through the book, I recommend doing a project or two. You can come up with an idea yourself, or you can follow one of the books that guide you through it.

For example:

  1. Learn Haskell by building a blog generator -> free, starts from 0 knowledge, and could even be used as the very first resource, instead of e.g. LYAH.
  2. The Simple Haskell Handbook -> not free, expects you to know the basics of Haskell already

Once you have more experience with projects, I would recommend re-reading your beginner book of choice. This time, you can skip the parts you already know and focus on what was confusing before. You will likely have a much easier time grasping those harder concepts.

p.s. If you are looking for a bit of extra motivation, check the blog post my teammate Shayne recently wrote about his journey with Haskell. He started in late 2021 and has already made huge progress!


Good luck with Haskell! If you have Haskell questions for me or the rest of the Wasp team, drop me a line at “martin” ++ “@” ++ concat [”wasp”, “-”, “lang”] <> “.dev” , or write to #haskell channel in Wasp-lang Discord server.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to get started with Haskell in 2022 (the straightforward way)

· 7 min read
Martin Sosic

Haskell is a unique and beautiful language that is worth learning, if for nothing else, then just for the concepts it introduces and their potential to expand your view on programming.

I have been programming in Haskell on and off since 2011 and professionally for the past 2 years, building a compiler. While in that time Haskell has become much more beginner-friendly, I keep seeing beginners who are overwhelmed by numerous popular options for build tools, installers, introductory educational resources, and similar. Haskell’s homepage getting a call from the previous decade to give them their UX back :D also doesn’t help!

That is why I decided to write this opinionated and practical post that will tell you exactly how to get started with Haskell in 2022 in the most standard / common way. Instead of worrying about decisions that you are not equipped to make at the moment (e.g. “what is the best build tool?”), you can focus on enjoying learning Haskell :)!

TLDR / Super opinionated summary

  1. For setup, use GHCup. Install GHC, HLS, and cabal.
  2. As a build tool, use cabal.
  3. For editor, use VS Code with Haskell extension. Or, use emacs/vim/....
  4. Join r/haskell. Feel free to ask for help!
  5. To learn the basics of Haskell, read the LYAH book and build a blog generator in Haskell. Focus on getting through stuff instead of understanding everything fully; you will come back to it later again.

1. Setup: Use GHCup for seamless installation

GHCup is a universal installer for Haskell. It will install everything you need to program in Haskell and will help you manage those installations in the future (update, switch versions, and similar). It is simple to use and works the same way on Linux, macOS, and Windows. It gives you a single central place/method to take care of your Haskell installation so that you don’t have to deal with OS-specific issues.

To install it, follow instructions at GHCup. Then, use it to install the Haskell Toolchain (aka stuff that you need to program in Haskell).

Haskell Toolchain consists of:

  1. GHC -> Haskell compiler
  2. HLS -> Haskell Language Server -> your code editor will use this to provide you with a great experience while editing Haskell code
  3. cabal -> Haskell build tool -> you will use this to organize your Haskell projects, build them, run them, define dependencies, etc.
  4. Stack -> cabal alternative, which you won’t need for now since we’ll go with cabal as our build tool of choice

2. Build tool: Use cabal

There are two popular build tools for Haskell: cabal and Stack. Both are widely used and have their pros and cons. So, one of the hard choices beginners often face is which one to use.

Some time ago, cabal was somewhat hard to use (complex, “dependency hell”). That’s why Stack was created: a user-friendly build tool that solves some of the common issues of cabal. (Interestingly, Stack uses cabal’s core library as its backend!) However, as Stack was being developed, cabal advanced, too. Many of its issues have been solved, making it a viable choice for beginners.

In 2022, I recommend cabal to beginners. I find it a bit easier to understand when starting out (no resolvers), it works well out of the box with GHCup and the rest of the ecosystem, and it seems to be better maintained lately.

3. Editor: VS Code is a safe bet

HLS (Haskell Language Server) brings all the cool IDE features to your editor. So, as long as your editor has a decent Haskell language extension that utilizes HLS, you are good.

The safest bet is to go with Visual Studio Code — it has a great Haskell extension that usually works out of the box. A lot of Haskell programmers also use Emacs and Vim. I can confirm they also have good support for Haskell.

4. Community: r/haskell and more

Haskell community is a great place to ask for help and learn about new developments in the ecosystem. I prefer r/haskell -> it tracks all the newest events and no question goes unanswered. There is also Haskell Discourse, where a lot of discussions happen, including the more official ones. A lot of Haskellers are still active on IRC, but I find it too complex and outdated to use.

Check https://www.haskell.org/community for a full list of Haskell communities.

5. Learning: You don’t need a math degree, just grab a book

There is a common myth going around that you need a special knowledge of math (PhD in category theory!) to be able to program in Haskell properly. From my experience, this is as far from the truth as it can be. It is certainly not needed, and I seriously doubt it helps even if you have it. Maybe for some very advanced Haskell stuff, but certainly not for junior/intermediate level.

Instead, learning Haskell is the same as learning other languages -> you need a healthy mix of theory and practice. The main difference is that there will be more unusual/new concepts than you are used to, which will require some additional effort. But these new concepts are also what makes learning Haskell so fun!

I recommend starting with a book for beginners, LYAH. It has an online version that you can read for free, or you can buy a printed version if you like physical books.

If you don't like LYAH, consider other popular books for beginners (none of them are free though):

  1. Haskell Programming from first principles
  2. Get Programming with Haskell
  3. Programming in Haskell

Whatever book you go with, don’t get stuck for too long on concepts that are confusing to you, especially towards the end of the book. Some concepts will just need time to click; don’t expect to grasp it all on the first try. Whatever you do grasp from the first read will likely be more than enough to get going with your first projects in Haskell. You can always come back to those complex concepts later and understand them better. Also, don’t be shy to ask the community -> there are many Haskellers out there happy to support you in your learning!

note

When I say "don't get stuck", I don't mean you should skip the difficult concept after the first hurdle. No, you should spend some hours experimenting, looking at it from different angles, playing with it, trying to crack it. But you shouldn't spend days trying to understand the same concept (e.g. function as a monad) and then feel defeated due to not grasping it 100%. Instead, if you put proper effort but stuff is not completely clicking, tap yourself on the back and move on for now.

Once you take the first pass through the book, I recommend doing a project or two. You can come up with an idea yourself, or you can follow one of the books that guide you through it.

For example:

  1. Learn Haskell by building a blog generator -> free, starts from 0 knowledge, and could even be used as the very first resource, instead of e.g. LYAH.
  2. The Simple Haskell Handbook -> not free, expects you to know the basics of Haskell already

Once you have more experience with projects, I would recommend re-reading your beginner book of choice. This time, you can skip the parts you already know and focus on what was confusing before. You will likely have a much easier time grasping those harder concepts.

p.s. If you are looking for a bit of extra motivation, check the blog post my teammate Shayne recently wrote about his journey with Haskell. He started in late 2021 and has already made huge progress!


Good luck with Haskell! If you have Haskell questions for me or the rest of the Wasp team, drop me a line at “martin” ++ “@” ++ concat [”wasp”, “-”, “lang”] <> “.dev” , or write to #haskell channel in Wasp-lang Discord server.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/05/dev-excuses-app-tutrial.html b/blog/2022/09/05/dev-excuses-app-tutrial.html index 3a5945a4a8..3116ccc95d 100644 --- a/blog/2022/09/05/dev-excuses-app-tutrial.html +++ b/blog/2022/09/05/dev-excuses-app-tutrial.html @@ -19,13 +19,13 @@ - - + +
-

Building an app to find an excuse for our sloppy work

· 8 min read

We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

curl -sSL https://get.wasp-lang.dev/installer.sh | sh

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Building an app to find an excuse for our sloppy work

· 8 min read

We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

curl -sSL https://get.wasp-lang.dev/installer.sh | sh

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/29/journey-to-1000-gh-stars.html b/blog/2022/09/29/journey-to-1000-gh-stars.html index df4329a7ec..0f9eae1d7b 100644 --- a/blog/2022/09/29/journey-to-1000-gh-stars.html +++ b/blog/2022/09/29/journey-to-1000-gh-stars.html @@ -19,13 +19,13 @@ - - + +
-

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

· 12 min read
Matija Sosic

Wasp is an open-source configuration language for building full-stack web apps that integrates with React & Node.js. We launched first prototype 2 years ago, currently are at 1.9k stars on GitHub and will be releasing Beta in the coming months.

It was very hard for us to find and be able to learn from early inception stories of successful OSS projects and that's why we want to share what it looked like for Wasp.

1k stars chart

Before the stars: Is this really a problem? (1 year)

My co-founder and twin brother Martin and I got an initial idea for Wasp in 2018, while developing a web platform for running bioinformatics analysis in the cloud for one London-based startup.

It was our third or fourth time creating a full-stack app from scratch with the latest & hottest stack. This time, it was React/Node.js; for our previous projects, we went through PHP/Java/Node.js on the back-end and jQuery/Backbone/Angular on the front-end. Because Martin and I felt we were spending a lot of time relearning how to use the latest stack just to build the same features all over again (auth, CRUD, forms, async jobs, etc.), we asked ourselves: Why not abstract these common functionalities in a stack-agnostic, higher-level language (like e.g. SQL does for databases) to never reimplement them again?

Before we jumped into coding, we wanted to make sure this is a problem that actually exists and that we understand it well (enough). In our previous startup we found Customer Development (aka talking to users) extremely helpful, so we decided to do it again for Wasp.

In a month or so we conducted 25 problem interviews, probing around “What is your biggest challenge with web app development?” After we compiled the results, we identified the following four problems as the most significant ones and decided to focus on them in our v1:

  • It is hard to quickly start a new web app and make sure the best practices are being followed.
  • There is a lot of duplication/boilerplate in managing the state across front-end, back-end, and the database.
  • A lot of common features are re-implemented for every new app.
  • Developers are overwhelmed by the increasing tool complexity and don't want to be responsible for managing it.

We also clustered the answers we got by topics, so we could dive deeper and identify the areas that got the most attention:

Start and setup of a web app - problems
Interviewee problems regarding starting and setting up a new web app.

The reason why we stopped at 25 was that the answers started repeating themselves. We felt that we identified the initial patterns and were ready to move on.

0-180 ⭐️: First Contact (7 months)

After confirming and clarifying the problem with other developers, Martin and I felt we finally deserved to do some coding. (Ok, I admit, we had actually already started, but the interviews made us feel better about it 😀). We created a new repo on GitHub and started setting up the tooling & playing around with the concept.

For the next couple of months, we treated Wasp as a side project/experiment and didn’t do any marketing. However, we were well aware of how crucial external feedback is. So, once we built a very rudimentary code generation functionality, we also created a project page that we could share with others to explain what we’re working on and ask for feedback.

At that point, we came up with the first “real” name for Wasp - STIC: Specification To Implementation Compiler, as the big vision for Wasp was to be a stack-agnostic, specification language from which we could generate the actual code in e.g. React & Node.js or even some other stack.

STIC - first project page
Our first page for Wasp! Not the best at explaining what Wasp does, though.

Baby steps on Reddit and Hacker News

Our preferred way of distributing STIC project page was through relevant subreddits - r/webdev, r/coding, r/javascript, r/Haskell, r/ProgrammingLanguages, ….

This was the first Reddit post we’ve ever made about Wasp:

First Wasp post on Reddit
Our first Reddit post! We managed to get some feedback before we got banned.

One important thing we learned is that Reddit doesn’t like self-promotion. Sometimes, even if you’re only asking for feedback, the mods (and bots) will see it as self-promo and ban your post. It depends a lot on the mods, though. Reaching out to them and asking for explanation sometimes helps, but not very often. All subreddits have their own rules and guidelines that describe when or how it is OK to post about your project (e.g., /r/webdev has “Showoff Saturdays”), and we tried to follow them as best as we could.

After Reddit, we also launched on HN. This was our first ever launch there! We scored 20 points and received a few motivating comments:

First Wasp post on Reddit

Listening to users

Martin and I also followed up with the people we had previously interviewed about their problems in web dev. We showed them STIC project page and asked for comments. From all the feedback we captured, we identified the following issues:

  • Developers were not familiar with a term “DSL.” Almost all of us use a DSL on a daily basis (e.g., SQL, HCL (Terraform), HTML), but it’s not a popular term.
  • Developers feared learning a new programming language. Although our goal was never to replace e.g. Java or Typescript but to make Wasp work alongside it, we discovered that we had failed to communicate it well. Our messaging made developers feel they have to drop all their previous knowledge and start from scratch if they want to use Wasp.
  • Nobody could try Wasp yet + there wasn’t any documentation besides the project page. Our code was public, but we didn’t have a build/distribution system yet. Only a devoted Haskell developer could build it from the source. This made it hard for developers to buy into the high-level vision, as there was nothing they could hold onto. Web frameworks/languages are very “tactile” — it’s hard to judge one without trying it out.

180-300 ⭐️ : Anybody can try Wasp out + Docs = Alpha! (3 months)

After processing this feedback, we realized that the next step for us was to get Wasp into the condition where developers can easily try it out without needing any extra knowledge or facing the trouble of compiling from the source. That meant polishing things a bit, adding a few crucial features, and writing our first documentation, so that users would know how to use it.

To write our docs, we picked Docusaurus — an OSS writing platform made by Facebook. We saw several other OSS projects using it for their docs + its ability to import React in your markdown was amazing. Docusaurus gave us a lot of initial structure, design and features (e.g., search), saving us from reinventing the wheel.

First Wasp docs
Martin made sure to add a huge Alpha warning sign :D

Our M.O. at the time was to focus pretty much exclusively on one thing, either development or community. Since Wasp team consisted of only Martin and me, it was really hard to do multiple things at once. After the docs were out and Wasp was ready to be easily downloaded, we called this version “Alpha” and switched once again into the “community” mode.

300-570 ⭐️ : Big break on Reddit and Product Hunt (2 months)

Once Alpha was out, we launched again on HackerNews and drew a bit of attention (34 upvotes and 3 comments). However, that was little compared to our Reddit launches, where we scored 263 upvotes on r/javascript and 365 upvotes on r/reactjs:

Big break on Reddit
They love me! [insert Tobey Maguire as Spiderman]

Compared to the volume of attention and feedback we’ve been previously receiving, this was a big surprise for us! Here are some of the changes in messaging that we made for the Reddit launches:

  • Put prefix “declarative” in front of the “language” to convey that it’s not a regular programming language like Python or Javascript but rather something much more lightweight and specialized.
  • Emphasized that Wasp is not a standalone language that will replace your current stack but rather a “glue” between your React & Node.js code, allowing you to keep using your favourite stack.
  • Focused on the benefits like “less boilerplate,” which is a well known pain in web development.
Docs made the difference

Once we added the docs, we noticed a peculiar thing: developers became much less trigger-happy to criticize the project, especially in a non-constructive way. Our feeling was the majority of developers who were checking Wasp out still didn’t read the docs in detail (or at all), but the sheer existence of them made them feel there is more content they should go through before passing the final judgment.

Winning #1 Product of The Day on Product Hunt

After HN and Reddit, we continued with the “Alpha launch” mindset and set ourselves to launch Wasp on Product Hunt. It was our first time ever launching on PH, so we didn’t know what to expect. We googled some advice, did maybe a week of preparation (i.e., wrote the copy, asked a few friends to share their experiences with Wasp once we’re live), and that was it.

We launched Wasp on PH on Dec 6, 2020 and it ended up as Product of the day! That gave us a boost in stars and overall traction. Another benefit of PH was that Wasp also ended up in their daily newsletter, which supposedly has over a million subscribers. All this gave us quite a boost and visibility increase.

Product Hunt launch

570-1000 ⭐️ : Wasp joins YC + “Official” HN launch (2.5 months)

Soon after Product Hunt, Wasp joined Y Combinator for their W21 batch. We had applied two times before and always made it to the interviews, but did not get in. This time, the traction tipped the scales in our favour. (You can read more about our journey to YC here.)

For the first month of YC, there was a lot of admin and setup work to deal with alongside the regular program. That added a third dimension to our existing two areas of effort. Once we went past that, we could again put more focus on product and community development.

Our next milestone was to launch Wasp on Hacker News, but this time “officially” as a YC-backed company. Hacker News provides a lot of good tips on how to successfully launch and 80% of the advice applies even if your product isn’t backed by YC. I wish I had known about it before. The gist of the advice is to write in a clear and succinct way and to avoid buzzwords, superlatives, and salesy tone above all. Consider HN readers as your peers and explain what you do in a way you would talk to a friend over a drink. It really works that way.

We went through the several iterations of the text, sweated over how it’s gonna go, and when the day finally came — we launched! It went beyond all our expectations. With 222 points and 79 comments, our HN launch was one of the most successful launches (#9) out of 300+ companies in the W21 batch. Many developers and VCs that checked our launch afterwards were surprised how much positive feedback Wasp received, especially given how honest and direct HN audience can be.

HN launch brought us about 200 stars right away, and the rest came in the following weeks. As it was February and the YC program was nearing its end, we needed to shift gears again and focus on fundraising. This put all the other efforts on the back burner. (You can read about our fundraising learnings from 250+ meetings in 98 days here.) But the interest of the community remained and even without much activity from our side they kept coming and trying Wasp out.

YC HN launch

Conclusion: understanding users > number of stars

Our primary goal was never to reach X stars, but rather to understand how we can make Wasp more helpful so that developers would want to use it for their projects. As you could read above, even well before we started a repository we made sure to talk to developers and learn about their problems.

We also kept continually improving how we present Wasp - had we not pivoted our message from “Wasp is a new programming language” to “Wasp is a simple config language that works alongside React & Node.js” we wouldn’t have been where we are today.

On the other hand, stars have become an unofficial “currency” of GitHub and developers and VCs alike consider it when evaluating a project. They shouldn’t be disregarded and you should make it easy for users who like your product to express their support by starring your repo (like I’m doing right here), but that should always be a second order of concern.

Good luck!

I hope you found this helpful and that we shed some light on how things can look like in the early stages of an OSS project. Also, keep in mind this was our singular experience and that every story is different, so take everything with a grain of salt and pick only what makes sense for you and your product.

We wish you the best of luck and feel free to reach out if you'll have any questions!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

· 12 min read
Matija Sosic

Wasp is an open-source configuration language for building full-stack web apps that integrates with React & Node.js. We launched first prototype 2 years ago, currently are at 1.9k stars on GitHub and will be releasing Beta in the coming months.

It was very hard for us to find and be able to learn from early inception stories of successful OSS projects and that's why we want to share what it looked like for Wasp.

1k stars chart

Before the stars: Is this really a problem? (1 year)

My co-founder and twin brother Martin and I got an initial idea for Wasp in 2018, while developing a web platform for running bioinformatics analysis in the cloud for one London-based startup.

It was our third or fourth time creating a full-stack app from scratch with the latest & hottest stack. This time, it was React/Node.js; for our previous projects, we went through PHP/Java/Node.js on the back-end and jQuery/Backbone/Angular on the front-end. Because Martin and I felt we were spending a lot of time relearning how to use the latest stack just to build the same features all over again (auth, CRUD, forms, async jobs, etc.), we asked ourselves: Why not abstract these common functionalities in a stack-agnostic, higher-level language (like e.g. SQL does for databases) to never reimplement them again?

Before we jumped into coding, we wanted to make sure this is a problem that actually exists and that we understand it well (enough). In our previous startup we found Customer Development (aka talking to users) extremely helpful, so we decided to do it again for Wasp.

In a month or so we conducted 25 problem interviews, probing around “What is your biggest challenge with web app development?” After we compiled the results, we identified the following four problems as the most significant ones and decided to focus on them in our v1:

  • It is hard to quickly start a new web app and make sure the best practices are being followed.
  • There is a lot of duplication/boilerplate in managing the state across front-end, back-end, and the database.
  • A lot of common features are re-implemented for every new app.
  • Developers are overwhelmed by the increasing tool complexity and don't want to be responsible for managing it.

We also clustered the answers we got by topics, so we could dive deeper and identify the areas that got the most attention:

Start and setup of a web app - problems
Interviewee problems regarding starting and setting up a new web app.

The reason why we stopped at 25 was that the answers started repeating themselves. We felt that we identified the initial patterns and were ready to move on.

0-180 ⭐️: First Contact (7 months)

After confirming and clarifying the problem with other developers, Martin and I felt we finally deserved to do some coding. (Ok, I admit, we had actually already started, but the interviews made us feel better about it 😀). We created a new repo on GitHub and started setting up the tooling & playing around with the concept.

For the next couple of months, we treated Wasp as a side project/experiment and didn’t do any marketing. However, we were well aware of how crucial external feedback is. So, once we built a very rudimentary code generation functionality, we also created a project page that we could share with others to explain what we’re working on and ask for feedback.

At that point, we came up with the first “real” name for Wasp - STIC: Specification To Implementation Compiler, as the big vision for Wasp was to be a stack-agnostic, specification language from which we could generate the actual code in e.g. React & Node.js or even some other stack.

STIC - first project page
Our first page for Wasp! Not the best at explaining what Wasp does, though.

Baby steps on Reddit and Hacker News

Our preferred way of distributing STIC project page was through relevant subreddits - r/webdev, r/coding, r/javascript, r/Haskell, r/ProgrammingLanguages, ….

This was the first Reddit post we’ve ever made about Wasp:

First Wasp post on Reddit
Our first Reddit post! We managed to get some feedback before we got banned.

One important thing we learned is that Reddit doesn’t like self-promotion. Sometimes, even if you’re only asking for feedback, the mods (and bots) will see it as self-promo and ban your post. It depends a lot on the mods, though. Reaching out to them and asking for explanation sometimes helps, but not very often. All subreddits have their own rules and guidelines that describe when or how it is OK to post about your project (e.g., /r/webdev has “Showoff Saturdays”), and we tried to follow them as best as we could.

After Reddit, we also launched on HN. This was our first ever launch there! We scored 20 points and received a few motivating comments:

First Wasp post on Reddit

Listening to users

Martin and I also followed up with the people we had previously interviewed about their problems in web dev. We showed them STIC project page and asked for comments. From all the feedback we captured, we identified the following issues:

  • Developers were not familiar with a term “DSL.” Almost all of us use a DSL on a daily basis (e.g., SQL, HCL (Terraform), HTML), but it’s not a popular term.
  • Developers feared learning a new programming language. Although our goal was never to replace e.g. Java or Typescript but to make Wasp work alongside it, we discovered that we had failed to communicate it well. Our messaging made developers feel they have to drop all their previous knowledge and start from scratch if they want to use Wasp.
  • Nobody could try Wasp yet + there wasn’t any documentation besides the project page. Our code was public, but we didn’t have a build/distribution system yet. Only a devoted Haskell developer could build it from the source. This made it hard for developers to buy into the high-level vision, as there was nothing they could hold onto. Web frameworks/languages are very “tactile” — it’s hard to judge one without trying it out.

180-300 ⭐️ : Anybody can try Wasp out + Docs = Alpha! (3 months)

After processing this feedback, we realized that the next step for us was to get Wasp into the condition where developers can easily try it out without needing any extra knowledge or facing the trouble of compiling from the source. That meant polishing things a bit, adding a few crucial features, and writing our first documentation, so that users would know how to use it.

To write our docs, we picked Docusaurus — an OSS writing platform made by Facebook. We saw several other OSS projects using it for their docs + its ability to import React in your markdown was amazing. Docusaurus gave us a lot of initial structure, design and features (e.g., search), saving us from reinventing the wheel.

First Wasp docs
Martin made sure to add a huge Alpha warning sign :D

Our M.O. at the time was to focus pretty much exclusively on one thing, either development or community. Since Wasp team consisted of only Martin and me, it was really hard to do multiple things at once. After the docs were out and Wasp was ready to be easily downloaded, we called this version “Alpha” and switched once again into the “community” mode.

300-570 ⭐️ : Big break on Reddit and Product Hunt (2 months)

Once Alpha was out, we launched again on HackerNews and drew a bit of attention (34 upvotes and 3 comments). However, that was little compared to our Reddit launches, where we scored 263 upvotes on r/javascript and 365 upvotes on r/reactjs:

Big break on Reddit
They love me! [insert Tobey Maguire as Spiderman]

Compared to the volume of attention and feedback we’ve been previously receiving, this was a big surprise for us! Here are some of the changes in messaging that we made for the Reddit launches:

  • Put prefix “declarative” in front of the “language” to convey that it’s not a regular programming language like Python or Javascript but rather something much more lightweight and specialized.
  • Emphasized that Wasp is not a standalone language that will replace your current stack but rather a “glue” between your React & Node.js code, allowing you to keep using your favourite stack.
  • Focused on the benefits like “less boilerplate,” which is a well known pain in web development.
Docs made the difference

Once we added the docs, we noticed a peculiar thing: developers became much less trigger-happy to criticize the project, especially in a non-constructive way. Our feeling was the majority of developers who were checking Wasp out still didn’t read the docs in detail (or at all), but the sheer existence of them made them feel there is more content they should go through before passing the final judgment.

Winning #1 Product of The Day on Product Hunt

After HN and Reddit, we continued with the “Alpha launch” mindset and set ourselves to launch Wasp on Product Hunt. It was our first time ever launching on PH, so we didn’t know what to expect. We googled some advice, did maybe a week of preparation (i.e., wrote the copy, asked a few friends to share their experiences with Wasp once we’re live), and that was it.

We launched Wasp on PH on Dec 6, 2020 and it ended up as Product of the day! That gave us a boost in stars and overall traction. Another benefit of PH was that Wasp also ended up in their daily newsletter, which supposedly has over a million subscribers. All this gave us quite a boost and visibility increase.

Product Hunt launch

570-1000 ⭐️ : Wasp joins YC + “Official” HN launch (2.5 months)

Soon after Product Hunt, Wasp joined Y Combinator for their W21 batch. We had applied two times before and always made it to the interviews, but did not get in. This time, the traction tipped the scales in our favour. (You can read more about our journey to YC here.)

For the first month of YC, there was a lot of admin and setup work to deal with alongside the regular program. That added a third dimension to our existing two areas of effort. Once we went past that, we could again put more focus on product and community development.

Our next milestone was to launch Wasp on Hacker News, but this time “officially” as a YC-backed company. Hacker News provides a lot of good tips on how to successfully launch and 80% of the advice applies even if your product isn’t backed by YC. I wish I had known about it before. The gist of the advice is to write in a clear and succinct way and to avoid buzzwords, superlatives, and salesy tone above all. Consider HN readers as your peers and explain what you do in a way you would talk to a friend over a drink. It really works that way.

We went through the several iterations of the text, sweated over how it’s gonna go, and when the day finally came — we launched! It went beyond all our expectations. With 222 points and 79 comments, our HN launch was one of the most successful launches (#9) out of 300+ companies in the W21 batch. Many developers and VCs that checked our launch afterwards were surprised how much positive feedback Wasp received, especially given how honest and direct HN audience can be.

HN launch brought us about 200 stars right away, and the rest came in the following weeks. As it was February and the YC program was nearing its end, we needed to shift gears again and focus on fundraising. This put all the other efforts on the back burner. (You can read about our fundraising learnings from 250+ meetings in 98 days here.) But the interest of the community remained and even without much activity from our side they kept coming and trying Wasp out.

YC HN launch

Conclusion: understanding users > number of stars

Our primary goal was never to reach X stars, but rather to understand how we can make Wasp more helpful so that developers would want to use it for their projects. As you could read above, even well before we started a repository we made sure to talk to developers and learn about their problems.

We also kept continually improving how we present Wasp - had we not pivoted our message from “Wasp is a new programming language” to “Wasp is a simple config language that works alongside React & Node.js” we wouldn’t have been where we are today.

On the other hand, stars have become an unofficial “currency” of GitHub and developers and VCs alike consider it when evaluating a project. They shouldn’t be disregarded and you should make it easy for users who like your product to express their support by starring your repo (like I’m doing right here), but that should always be a second order of concern.

Good luck!

I hope you found this helpful and that we shed some light on how things can look like in the early stages of an OSS project. Also, keep in mind this was our singular experience and that every story is different, so take everything with a grain of salt and pick only what makes sense for you and your product.

We wish you the best of luck and feel free to reach out if you'll have any questions!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/10/28/farnance-hackathon-winner.html b/blog/2022/10/28/farnance-hackathon-winner.html index 74ee554b3f..d77df09c2a 100644 --- a/blog/2022/10/28/farnance-hackathon-winner.html +++ b/blog/2022/10/28/farnance-hackathon-winner.html @@ -19,13 +19,13 @@ - - + +
-

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

· 4 min read
Matija Sosic

farnance hero shot

Julian LaNeve is an engineer and data scientist who currently works at Astronomer.io as a Product Manager. In his free time, he enjoys playing poker, chess and winning data science competitions.

His project, Farnance, is a SaaS marketplace that allows farmers to transform their production into a digital asset on blockchain. Julian and his team developed Farnance as a part of the London Business School’s annual hackathon HackLBS 2021, and ended up as winners among more than 250 participants competing for 6 prizes in total!

Read on to learn why Julian chose Wasp to develop and deploy Farnance and what parts he enjoyed the most.

Finding a perfect React & Node.js hackathon setup

Julian had previous experiences with React and Node.js and loved that he could use JavaScript across the stack, but setting up a new project and making sure it uses all the latest packages (and then also figuring out how to deploy it) was always a pain. Since the hackathon only lasted for two days, he needed a quick way to get started but still have the freedom to use his favourite stack.

The power of one-line auth and No-API approach

Julian first learned about Wasp when it launched on HN and decided it would be a perfect tool for his case. The whole app setup, across the full stack, is covered out-of-the-box, simply by typing wasp new farnance, and he is ready to start writing own React & Node.js code.

Except on the app setup, the team saved a ton of time by not needing to implement the authentication and a typical CRUD API, since it is covered by Wasp as well. They could also deploy everything for free on Heroku and Netlify in just a few steps, which was a perfect fit for a hackathon.

Julian's testimonial on Discord

Farnance is still running and you can try it out here! The source code is also publicly available, although note it is running on older version of Wasp so some things are a bit different.

Spend more time developing features and less time reinventing the wheel

Julian was amazed by how fast he was able to get Farnance of the ground and share a working web app with the users! He decided to go with Google's material-ui for an UI framework which gave his app an instant professional look, although they didn’t have a dedicated designer on the team.

With all the common web app features (setup, auth, CRUD API) being taken care of by Wasp out-of-the-box they could invest all the time saved in developing and refining their unique features which in the end brought them victory!

I’ve done plenty of hackathons before where I’ve built small SaaS apps, and there’s just so much time wasted setting up common utilities - stuff like user management, databases, routing, etc. Wasp handled all that for me and let me build out our web app in record time

— Julian LaNeve - Farnance

Farnance's dashboard
Farnance dashboard in action!

Start quickly, but also scale without worries

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

Since Wasp compiler generates a full-stack React & Node.js app under the hood, there aren’t any technical limitations to scaling Julian’s app as it grows and gets more users in the future. By running wasp build inside a project folder, developers gets both frontend files and a Dockerfile for the backend, which can then be deployed as any regular web app to the platform of your choice.

Wasp provides step-by step instructions on how to do it with Netlify and Fly.io for free, but we plan to add even more examples and more integrated deployment experience in the coming releases!

Deploying the wasp app was incredibly easy - I didn’t have time to stand up full infrastructure in the 2 day hackathon and don’t have an infra/devops background, but I had something running on Netlify within an hour. Other projects at the hackathon struggled to do this, and putting access in the hands of the judges certainly helped get us 1st place.

— Julian LaNeve - Farnance

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

· 4 min read
Matija Sosic

farnance hero shot

Julian LaNeve is an engineer and data scientist who currently works at Astronomer.io as a Product Manager. In his free time, he enjoys playing poker, chess and winning data science competitions.

His project, Farnance, is a SaaS marketplace that allows farmers to transform their production into a digital asset on blockchain. Julian and his team developed Farnance as a part of the London Business School’s annual hackathon HackLBS 2021, and ended up as winners among more than 250 participants competing for 6 prizes in total!

Read on to learn why Julian chose Wasp to develop and deploy Farnance and what parts he enjoyed the most.

Finding a perfect React & Node.js hackathon setup

Julian had previous experiences with React and Node.js and loved that he could use JavaScript across the stack, but setting up a new project and making sure it uses all the latest packages (and then also figuring out how to deploy it) was always a pain. Since the hackathon only lasted for two days, he needed a quick way to get started but still have the freedom to use his favourite stack.

The power of one-line auth and No-API approach

Julian first learned about Wasp when it launched on HN and decided it would be a perfect tool for his case. The whole app setup, across the full stack, is covered out-of-the-box, simply by typing wasp new farnance, and he is ready to start writing own React & Node.js code.

Except on the app setup, the team saved a ton of time by not needing to implement the authentication and a typical CRUD API, since it is covered by Wasp as well. They could also deploy everything for free on Heroku and Netlify in just a few steps, which was a perfect fit for a hackathon.

Julian's testimonial on Discord

Farnance is still running and you can try it out here! The source code is also publicly available, although note it is running on older version of Wasp so some things are a bit different.

Spend more time developing features and less time reinventing the wheel

Julian was amazed by how fast he was able to get Farnance of the ground and share a working web app with the users! He decided to go with Google's material-ui for an UI framework which gave his app an instant professional look, although they didn’t have a dedicated designer on the team.

With all the common web app features (setup, auth, CRUD API) being taken care of by Wasp out-of-the-box they could invest all the time saved in developing and refining their unique features which in the end brought them victory!

I’ve done plenty of hackathons before where I’ve built small SaaS apps, and there’s just so much time wasted setting up common utilities - stuff like user management, databases, routing, etc. Wasp handled all that for me and let me build out our web app in record time

— Julian LaNeve - Farnance

Farnance's dashboard
Farnance dashboard in action!

Start quickly, but also scale without worries

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

Since Wasp compiler generates a full-stack React & Node.js app under the hood, there aren’t any technical limitations to scaling Julian’s app as it grows and gets more users in the future. By running wasp build inside a project folder, developers gets both frontend files and a Dockerfile for the backend, which can then be deployed as any regular web app to the platform of your choice.

Wasp provides step-by step instructions on how to do it with Netlify and Fly.io for free, but we plan to add even more examples and more integrated deployment experience in the coming releases!

Deploying the wasp app was incredibly easy - I didn’t have time to stand up full infrastructure in the 2 day hackathon and don’t have an infra/devops background, but I had something running on Netlify within an hour. Other projects at the hackathon struggled to do this, and putting access in the hands of the judges certainly helped get us 1st place.

— Julian LaNeve - Farnance

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/15/auth-feature-announcement.html b/blog/2022/11/15/auth-feature-announcement.html index ca345a5123..4526ab5af5 100644 --- a/blog/2022/11/15/auth-feature-announcement.html +++ b/blog/2022/11/15/auth-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - New auth method (Google)

· 4 min read
Shayne Czyzewski

No login for you!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Prologue

We've all been there. Your app needs to support user authentication with social login, and you must now decide what to do next. Should you eschew the collective experience and wisdom of the crowd and YOLO it by rolling your own, praying you don't get pwned in prod? "Nah, I just ate some week-old sushi and can't take another risk that big anytime soon.", you rightly think.

Ok, surely you can just use a library, right? Open source software, baby! "Hmm, seems Library X, Y, and Z are all somewhat used, each with their pros/cons, nuances, and integration pain points. Oh wait, there are tutorials for each... but each says how hard they are to correctly set up and use. I scoped this feature for one day, not a one-week hair-pulling adventure (Dang scrum! Who likes it anyways? Oh yeah, PMs do. Dang PMs!)." Ok, something else. You need to brainstorm. You instead start to surf Twitter and see an ad for some unicorn auth startup.

Eureka, you can go with a third-party SaaS offering! "We shouldn't have to pay for a while (I think? hope!), and it's just another dependency, no biggie... #microservices, right?" "But what about outages, data privacy, mapping users between systems, and all that implicit trust you are placing in them?" you think. "What happens when Elon buys them next?" You gasp as if you walked by a Patagonia vest covered in that hot new Burnt Hair cologne.

"All I want is username and password auth with Google login support, why is that so hard in 2022?!? I miss Basic HTTP auth headers. I think I'll move off the grid and become a woodworker."

Easy auth setup in Wasp

Wasp helps that dev by taking care of the entire auth setup process out of the box. Adding support for username and password auth, plus Google login, is super quick and easy for Wasp apps. We think this makes adding auth fast and convenient, with no external dependencies or frustrating manual configuration. Here’s how it works:

Step 1 - Add the appropriate models

We need to store user info and the external mapping association for social logins. Here is an example you can start from and add new fields to:

./main.wasp
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

Step 2 - Update app.auth to use these items

./main.wasp
app authExample {
// ...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login"
}
}

Step 3 - Get Google credentials and add environment variables

Follow the Google setup guide here and add the environment variables to your .env.server file.

Step 4 - Make use of the Google login button in your Login page component

./src/client/auth/Login.js
import React from 'react'
import { Link } from 'react-router-dom'

import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
import LoginForm from '@wasp/auth/forms/Login'

const Login = () => {
return (
<div>
<div>
<LoginForm/>
</div>
<div>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</div>
<div>
<GoogleSignInButton/>
</div>
</div>
)
}

export default Login

Step 5 - Run the app!

Epilogue

No need to move off the grid out of frustration when adding authentication and social login to your web app. Here is a complete, minimal example if you want to jump right in, and here are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - New auth method (Google)

· 4 min read
Shayne Czyzewski

No login for you!

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

Prologue

We've all been there. Your app needs to support user authentication with social login, and you must now decide what to do next. Should you eschew the collective experience and wisdom of the crowd and YOLO it by rolling your own, praying you don't get pwned in prod? "Nah, I just ate some week-old sushi and can't take another risk that big anytime soon.", you rightly think.

Ok, surely you can just use a library, right? Open source software, baby! "Hmm, seems Library X, Y, and Z are all somewhat used, each with their pros/cons, nuances, and integration pain points. Oh wait, there are tutorials for each... but each says how hard they are to correctly set up and use. I scoped this feature for one day, not a one-week hair-pulling adventure (Dang scrum! Who likes it anyways? Oh yeah, PMs do. Dang PMs!)." Ok, something else. You need to brainstorm. You instead start to surf Twitter and see an ad for some unicorn auth startup.

Eureka, you can go with a third-party SaaS offering! "We shouldn't have to pay for a while (I think? hope!), and it's just another dependency, no biggie... #microservices, right?" "But what about outages, data privacy, mapping users between systems, and all that implicit trust you are placing in them?" you think. "What happens when Elon buys them next?" You gasp as if you walked by a Patagonia vest covered in that hot new Burnt Hair cologne.

"All I want is username and password auth with Google login support, why is that so hard in 2022?!? I miss Basic HTTP auth headers. I think I'll move off the grid and become a woodworker."

Easy auth setup in Wasp

Wasp helps that dev by taking care of the entire auth setup process out of the box. Adding support for username and password auth, plus Google login, is super quick and easy for Wasp apps. We think this makes adding auth fast and convenient, with no external dependencies or frustrating manual configuration. Here’s how it works:

Step 1 - Add the appropriate models

We need to store user info and the external mapping association for social logins. Here is an example you can start from and add new fields to:

./main.wasp
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

Step 2 - Update app.auth to use these items

./main.wasp
app authExample {
// ...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login"
}
}

Step 3 - Get Google credentials and add environment variables

Follow the Google setup guide here and add the environment variables to your .env.server file.

Step 4 - Make use of the Google login button in your Login page component

./src/client/auth/Login.js
import React from 'react'
import { Link } from 'react-router-dom'

import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
import LoginForm from '@wasp/auth/forms/Login'

const Login = () => {
return (
<div>
<div>
<LoginForm/>
</div>
<div>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</div>
<div>
<GoogleSignInButton/>
</div>
</div>
)
}

export default Login

Step 5 - Run the app!

Epilogue

No need to move off the grid out of frustration when adding authentication and social login to your web app. Here is a complete, minimal example if you want to jump right in, and here are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/16/alpha-testing-program-post-mortem.html b/blog/2022/11/16/alpha-testing-program-post-mortem.html index 0a6635f4d4..baf47deb18 100644 --- a/blog/2022/11/16/alpha-testing-program-post-mortem.html +++ b/blog/2022/11/16/alpha-testing-program-post-mortem.html @@ -19,13 +19,13 @@ - - + +
-

Alpha Testing Program: post-mortem

· 7 min read
Matija Sosic

We are working on a new web framework that integrates with React & Node.js, and also happens to be a language. As you can probably imagine, it’s not easy to get people to use a new piece of technology, especially while still in Alpha. On the other hand, without users and their feedback, it’s impossible to know what to build.

That is why we ran Alpha Testing Program for Wasp - here is what we learned and what went both well and wrong along the way.

twitter DM - shared atp in swag groups

“Of course I know about Wasp! I just haven’t come around to trying it out yet.”

Although we hit the front page of HN several times and are about to reach 2,000 stars on GitHub, there is still a big difference between a person starring a repo and actually sitting down and building something with it.

Talking to people, we realised a lot of them had heard of Wasp, thought it was a neat idea, but hadn’t tried it out. These were the main reasons:

  • having to find 30 mins to go through our Build a Todo App tutorial - “I'm busy now, but I’ll do it next week.”
  • building a bare-bones todo app is not that exciting
  • not having an idea what else to build
  • “the product is still in alpha, so I will bookmark it for later”

These are all obvious and understandable reasons. I must admit, I’m much the same — maybe even worse — when it comes to trying out something new/unproven. It just isn’t a priority, and without a push that will help me overcome all these objections, I usually don’t have an incentive to go through with it.

Having realised all that, we understood we needed to give people a reason to try Wasp out now, because that’s when we needed the feedback, not next week.

Welcome to Wasp Alpha Testing Program!

The team
I was having a bit too much fun here, but Portal fans will understand.

We quickly put together an admissions page for alpha testers in Notion (you can see it here) and started sharing it around. To counter the hurdles we mentioned above, we time-boxed the program (”this is happening now and you have 48 hours to finish once you start”) and promised a t-shirt to everyone that goes through the tutorial and fills out the feedback form.

Apply to ATP - CTA
CTA from the admissions page

Soon, the first applications started trickling in! For each new applicant, we’d follow up with the instructions on how to successfully go through the Alpha Testing Program:

  • fill out intro form (years of experience, preferred stack, etc)
  • go through our “build a Todo app” tutorial
  • fill out the feedback form - what was good, what was bad etc.

Timeboxing
People were really respectful of this deadline and would politely ask to extend it in case they couldn’t make it.

But, soon after I got the following message on Twitter:

twitter DM - shared atp in swag groups

We got really scared that we would get a ton of folks putting in minimal effort while trying Wasp out just to get the free swag, leaving us empty-handed and having learned nothing! On the other hand, we didn’t have much choice since we didn’t define the “minimum required quality” of feedback in advance.

Luckily, it wasn’t the problem in the end, even the opposite -- we did get a surge of applications, but only a portion of them finished the program and the ones that did left really high-quality feedback!

How it went - test profile & feedback

Tester profile

We received 210 applications and 53 out of those completed the program — 25% completion rate.

We also surveyed applicants about their preferred stack, years of programming experience, etc:

Intro survey - tester profile
Yep, we like puns.

The feedback

The feedback form evaluated testers’ overall experience with Wasp. We asked them what they found to be the best and worst parts of working with Wasp, as well as about the next features they’d like to see.

Feedback survey - experience

The bad parts

What our testers were missing the most was a full-blown IDE and TypeScript support. Both of these are coming in Beta but only JS was supported at the time. Plus, there were some installation problems with Windows (which is not fully supported yet — best to use it through WSL).

Feedback survey - the bad parts

We were already aware that TypeScript support is an important feature, but didn’t have an exact feeling of how much - the feedback was really helpful and helped us prioritise our Beta backlog.

The good parts

Testers’ favourite part was the batteries-included experience, particularly the auth model.

Feedback survey - the good parts

Post-mortem: what didn’t go well

No threshold for feedback quality

Feedback quality

We didn’t put any kind of restrictions on the feedback form, e.g. minimal length of the feedback. That resulted in ~15%-20% of answers being single words, such as depicted above. I’m not sure if there is an efficient way to avoid this or just a stat to live with.

Using free text form for collecting addresses

It never crossed our minds before that validating addresses could be such an important part of shipping swag, but turns out it is. It seems that there are a lot of ways to specify an address, some of which are different from what is expected by our post office, resulting in a number of shipments getting returned.

An ideal solution would be to use a specialized “address” field in a survey that would auto-validate it, but turns out Typeform (which we used) doesn’t have that feature implemented yet, although it’s been highly requested.

Shipment returned

Shipment returned email

The non-obvious benefit of Alpha Testing Program

What went well is that we got a lot of high-quality feedback that steered and fortified our plan for the upcoming Beta release.

The other big benefit is that we finally solved the “looks cool but i’ll try it out later maybe” problem. Overall, our usage went well up during the program, but even after it ended, the baseline increased significantly. This was the second-order effect we didn’t foresee.

Our understanding is that once people finally gave it a try, a portion of them felt the value first-hand and decided to keep using it for other projects as well.

Alpha testing program - usage spike

Summary & going forward: Beta

The overall conclusion from our Alpha Testing Program is it was a worthy effort which got us valuable feedback and positively affected the overall usage. Moving forward we’ll try to focus on ensuring more quality feedback and prioritising 1-to-1 communication to make sure we fully understand what bothers Wasp users and what we can improve. It also might be helpful to do testing in smaller batches so we are not overwhelmed with responses and can focus on the individual testers - that’s something we might try out in Beta.

As mentioned, the next stop is Beta! It comes out on the 27th of November - sign up here to get notified.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Alpha Testing Program: post-mortem

· 7 min read
Matija Sosic

We are working on a new web framework that integrates with React & Node.js, and also happens to be a language. As you can probably imagine, it’s not easy to get people to use a new piece of technology, especially while still in Alpha. On the other hand, without users and their feedback, it’s impossible to know what to build.

That is why we ran Alpha Testing Program for Wasp - here is what we learned and what went both well and wrong along the way.

twitter DM - shared atp in swag groups

“Of course I know about Wasp! I just haven’t come around to trying it out yet.”

Although we hit the front page of HN several times and are about to reach 2,000 stars on GitHub, there is still a big difference between a person starring a repo and actually sitting down and building something with it.

Talking to people, we realised a lot of them had heard of Wasp, thought it was a neat idea, but hadn’t tried it out. These were the main reasons:

  • having to find 30 mins to go through our Build a Todo App tutorial - “I'm busy now, but I’ll do it next week.”
  • building a bare-bones todo app is not that exciting
  • not having an idea what else to build
  • “the product is still in alpha, so I will bookmark it for later”

These are all obvious and understandable reasons. I must admit, I’m much the same — maybe even worse — when it comes to trying out something new/unproven. It just isn’t a priority, and without a push that will help me overcome all these objections, I usually don’t have an incentive to go through with it.

Having realised all that, we understood we needed to give people a reason to try Wasp out now, because that’s when we needed the feedback, not next week.

Welcome to Wasp Alpha Testing Program!

The team
I was having a bit too much fun here, but Portal fans will understand.

We quickly put together an admissions page for alpha testers in Notion (you can see it here) and started sharing it around. To counter the hurdles we mentioned above, we time-boxed the program (”this is happening now and you have 48 hours to finish once you start”) and promised a t-shirt to everyone that goes through the tutorial and fills out the feedback form.

Apply to ATP - CTA
CTA from the admissions page

Soon, the first applications started trickling in! For each new applicant, we’d follow up with the instructions on how to successfully go through the Alpha Testing Program:

  • fill out intro form (years of experience, preferred stack, etc)
  • go through our “build a Todo app” tutorial
  • fill out the feedback form - what was good, what was bad etc.

Timeboxing
People were really respectful of this deadline and would politely ask to extend it in case they couldn’t make it.

But, soon after I got the following message on Twitter:

twitter DM - shared atp in swag groups

We got really scared that we would get a ton of folks putting in minimal effort while trying Wasp out just to get the free swag, leaving us empty-handed and having learned nothing! On the other hand, we didn’t have much choice since we didn’t define the “minimum required quality” of feedback in advance.

Luckily, it wasn’t the problem in the end, even the opposite -- we did get a surge of applications, but only a portion of them finished the program and the ones that did left really high-quality feedback!

How it went - test profile & feedback

Tester profile

We received 210 applications and 53 out of those completed the program — 25% completion rate.

We also surveyed applicants about their preferred stack, years of programming experience, etc:

Intro survey - tester profile
Yep, we like puns.

The feedback

The feedback form evaluated testers’ overall experience with Wasp. We asked them what they found to be the best and worst parts of working with Wasp, as well as about the next features they’d like to see.

Feedback survey - experience

The bad parts

What our testers were missing the most was a full-blown IDE and TypeScript support. Both of these are coming in Beta but only JS was supported at the time. Plus, there were some installation problems with Windows (which is not fully supported yet — best to use it through WSL).

Feedback survey - the bad parts

We were already aware that TypeScript support is an important feature, but didn’t have an exact feeling of how much - the feedback was really helpful and helped us prioritise our Beta backlog.

The good parts

Testers’ favourite part was the batteries-included experience, particularly the auth model.

Feedback survey - the good parts

Post-mortem: what didn’t go well

No threshold for feedback quality

Feedback quality

We didn’t put any kind of restrictions on the feedback form, e.g. minimal length of the feedback. That resulted in ~15%-20% of answers being single words, such as depicted above. I’m not sure if there is an efficient way to avoid this or just a stat to live with.

Using free text form for collecting addresses

It never crossed our minds before that validating addresses could be such an important part of shipping swag, but turns out it is. It seems that there are a lot of ways to specify an address, some of which are different from what is expected by our post office, resulting in a number of shipments getting returned.

An ideal solution would be to use a specialized “address” field in a survey that would auto-validate it, but turns out Typeform (which we used) doesn’t have that feature implemented yet, although it’s been highly requested.

Shipment returned

Shipment returned email

The non-obvious benefit of Alpha Testing Program

What went well is that we got a lot of high-quality feedback that steered and fortified our plan for the upcoming Beta release.

The other big benefit is that we finally solved the “looks cool but i’ll try it out later maybe” problem. Overall, our usage went well up during the program, but even after it ended, the baseline increased significantly. This was the second-order effect we didn’t foresee.

Our understanding is that once people finally gave it a try, a portion of them felt the value first-hand and decided to keep using it for other projects as well.

Alpha testing program - usage spike

Summary & going forward: Beta

The overall conclusion from our Alpha Testing Program is it was a worthy effort which got us valuable feedback and positively affected the overall usage. Moving forward we’ll try to focus on ensuring more quality feedback and prioritising 1-to-1 communication to make sure we fully understand what bothers Wasp users and what we can improve. It also might be helpful to do testing in smaller batches so we are not overwhelmed with responses and can focus on the individual testers - that’s something we might try out in Beta.

As mentioned, the next stop is Beta! It comes out on the 27th of November - sign up here to get notified.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/16/tailwind-feature-announcement.html b/blog/2022/11/16/tailwind-feature-announcement.html index 74408e62a4..4aa06fb3e2 100644 --- a/blog/2022/11/16/tailwind-feature-announcement.html +++ b/blog/2022/11/16/tailwind-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - Tailwind CSS support

· 3 min read
Shayne Czyzewski

Full stack devs

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

There are backend devs who can do some frontend, and frontend devs who can do some backend. But the mythical full stack dev is exceedingly rare (or more likely, a lie). Even as someone who falls into the meme category above, we all still need to make websites that look noice. This is a place where CSS frameworks can help.

But which one should you use? According to our extensive research, a statistically-questionable-but-you’re-still-significant-to-us 11 people on Twitter wanted us to add better support for Tailwind. Which was lucky for us, since we already added it before asking them. 😅

Twitter voting

Ok, it wasn’t a huge stretch for us to do so preemptively. Tailwind is one of the most heavily used CSS frameworks out there today and seems to keep growing in popularity. So how do you integrate it into your Wasp apps? Like many things in Wasp, it’s really easy- just drop in two config files into the root of your project and you can then start using it! Here are the defaults:

./tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
./postcss.config.cjs
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

When these two files are present, Wasp will make sure all the required NPM dependencies get added, that PostCSS plays nicely with Tailwind directives in CSS files, and that your JavaScript files are properly processed so you can use all the CSS selectors you want (provided you are properly equipped :D).

Best monitor

With that in place, you can add the Tailwind directives to your CSS files like so:

./src/client/Main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* rest of content below */

And then start using Tailwind classes in your components:

<h1 className="text-3xl font-bold underline">
Hello world!
</h1>

As usual, Wasp will still automatically reload your code and refresh the browser on any changes. 🥳

Lastly, here is a small example that shows how to add a few Tailwind plugins for the adventurous (wasp file and Tailwind config), and here are the docs for more details. We can’t wait to see what you make!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - Tailwind CSS support

· 3 min read
Shayne Czyzewski

Full stack devs

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

There are backend devs who can do some frontend, and frontend devs who can do some backend. But the mythical full stack dev is exceedingly rare (or more likely, a lie). Even as someone who falls into the meme category above, we all still need to make websites that look noice. This is a place where CSS frameworks can help.

But which one should you use? According to our extensive research, a statistically-questionable-but-you’re-still-significant-to-us 11 people on Twitter wanted us to add better support for Tailwind. Which was lucky for us, since we already added it before asking them. 😅

Twitter voting

Ok, it wasn’t a huge stretch for us to do so preemptively. Tailwind is one of the most heavily used CSS frameworks out there today and seems to keep growing in popularity. So how do you integrate it into your Wasp apps? Like many things in Wasp, it’s really easy- just drop in two config files into the root of your project and you can then start using it! Here are the defaults:

./tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
./postcss.config.cjs
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

When these two files are present, Wasp will make sure all the required NPM dependencies get added, that PostCSS plays nicely with Tailwind directives in CSS files, and that your JavaScript files are properly processed so you can use all the CSS selectors you want (provided you are properly equipped :D).

Best monitor

With that in place, you can add the Tailwind directives to your CSS files like so:

./src/client/Main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* rest of content below */

And then start using Tailwind classes in your components:

<h1 className="text-3xl font-bold underline">
Hello world!
</h1>

As usual, Wasp will still automatically reload your code and refresh the browser on any changes. 🥳

Lastly, here is a small example that shows how to add a few Tailwind plugins for the adventurous (wasp file and Tailwind config), and here are the docs for more details. We can’t wait to see what you make!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/17/hacktoberfest-wrap-up.html b/blog/2022/11/17/hacktoberfest-wrap-up.html index bd3e50be64..cbcc767fd3 100644 --- a/blog/2022/11/17/hacktoberfest-wrap-up.html +++ b/blog/2022/11/17/hacktoberfest-wrap-up.html @@ -19,15 +19,15 @@ - - + +
-

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

· 6 min read

2078 lines of code across 24 PRs were changed in Wasp repo during HacktoberFest 2022 - the most prominent online event for promoting and celebrating OSS culture. October has been a blast, to say the least, and the most active month in the repo's history.

This is the story of our journey along with the tips on leveraging Hacktoberfest to get your repo buzzing! 🐝🐝

How it went: the stats

Let's take a quick look at the charts below (data obtained from OSS Insight platform) 👇

PR history
24 contributor PRs in Oct, an all-time high!

Lines of code changes
On the other hand, number of changed LoC isn't that huge

While the number of PRs is at an all-time high, the number of updated lines of code is fewer than usual. If we take a look at the distribution of PR sizes in the first chart, we can see that "xs" and "s" PRs are in the majority (20 out of 24).

This brings us to our first conclusion: first-time contributors start with small steps! The main benefit here is getting potential contributors interested and familiar with the project, rather than expecting them to jump in and +

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

· 6 min read

2078 lines of code across 24 PRs were changed in Wasp repo during HacktoberFest 2022 - the most prominent online event for promoting and celebrating OSS culture. October has been a blast, to say the least, and the most active month in the repo's history.

This is the story of our journey along with the tips on leveraging Hacktoberfest to get your repo buzzing! 🐝🐝

How it went: the stats

Let's take a quick look at the charts below (data obtained from OSS Insight platform) 👇

PR history
24 contributor PRs in Oct, an all-time high!

Lines of code changes
On the other hand, number of changed LoC isn't that huge

While the number of PRs is at an all-time high, the number of updated lines of code is fewer than usual. If we take a look at the distribution of PR sizes in the first chart, we can see that "xs" and "s" PRs are in the majority (20 out of 24).

This brings us to our first conclusion: first-time contributors start with small steps! The main benefit here is getting potential contributors interested and familiar with the project, rather than expecting them to jump in and immediately start implementing the next major feature. Efforts like that require investing time to understand and digest codebase architecture, design decisions and the development process.

On the other hand, being able to implement and merge any feature, no matter the size, from beginning to the end, and to get your name on the list of contributors of your favourite project is an amazing feeling! That will make your contributors feel like superheroes and motivate them to keep taking on larger and larger chunks, and maybe eventually even join the core team!

Thus, the second conclusion would be: don’t underestimate the significance of small PRs! It's not about reducing your backlog, but rather encouraging developers to get engaged with your project in a friendly way.

tip

To make it easier for your new contributors, you can prepare in advance good issues to get started with - e.g. smaller bugs, docs improvements, fun but isolated problems, etc.

We added good-first-issue label to such issues in Wasp repo, and even added extra context such as no-haskell, webdev, example, docs.

With your repo being set, the next question is "How do I get people to pick my project to work on"? Relying solely on putting "Hacktoberfest" topic on your GitHub repo won't do the trick, not with thousands of other repos doing the same.

If you want to get noticed, you need to do marketing. A lot of it. The name of the game here is what you put in is what you get back. Let's talk about this in more detail.

A thin line between genuine interactions and annoying self-promotion

First and foremost, you'll need to create an entry point with all the necessary information for the participants. We opted for a GitHub issue where we categorized Hacktoberfest issues by type, complexity, etc, but it can be anything - a dedicated landing page, Medium/Dev.to article, or whatever works for you. Once you have that, you can start promoting it.

Hacktoberfest entry point - gh issue
Our entry point for Hacktoberfest

Our marketing strategy consisted of the following:

  1. Tweeting regularly - what's new, interesting issues, ...

  2. Writing meaningful Reddit posts about your achievements

  3. Hanging out in HacktoberFest Discord server, chatting with others and answering their questions

  4. Checking posts with appropriate tags on different blogging websites like Medium, Dev.to, Hashnode, etc. and participating in conversations.

There are plenty of other ways to advertise your project, like joining events or writing articles. Even meme contests. The activities mentioned above worked the best for us. Let’s dive a bit deeper.

Tweets are pretty obvious - as mentioned, you can share updates on how stuff is going. Tag contributors, inform your followers about available issues and mention those who might be a good fit for tackling them.

Reddit is a much more complex beast. You need to avoid clickbait post titles, comply with subreddit rules on self-promotion and try to give meaningful info to the community simultaneously. Take less than you give, and you’re good.

posting on reddit
How posting on Reddit feels

The Discord server marketing was pretty straightforward. There’s even a dedicated channel for self-promotion. In case you're not talkative much, dropping a link to your project is OK, and that’s it. On the other hand, the server is an excellent platform for discussing Hacktoberfest-related issues, approaches, and ideas. The more you chat, the higher your chances of drawing attention to your project.

The most engaging but also time consuming activity was commenting on blog posts of other Hacktoberfest participants. Pretending that you’re interested in the topic only to leave a self-promoting comment will not bring you anywhere - it can only result in your comment being removed. Make sure to provide value: add more information on the topic of the article, address specific points the author may have missed, or mention how you’ve dealt with the related issue in your project.

Be consistent and dedicate time to regularly to check new articles and jump into discussions. Share a link to your repo only if it fits into the flow of the conversation.

Content marketing in a nutshell

Was it worth it?

Before joining HacktoberFest as maintainers, we weren’t sure it would be worth the time investment. Our skepticism was reinforced by the following:

  1. Mentions of people submitting trivial PRs just to win the award

  2. The fact that we're making a relatively complex project (DSL for developing React + Node.js full-stack web apps with less code) and it might be hard for people to get into it

  3. The compiler is written is Haskell, with templates in JavaScript - again, not the very common project setup

Fortunately, none of this turned out to be a problem! We've got 24 valid PRs, both Haskell and non-Haskell, a ton of valuable feedback, and several dozen new users and community members.

Wrap up

Don’t expect magic to happen. HacktoberFest is all about smaller changes and getting community introduced to your project. Be ready to promote your repo genuinely and don’t be afraid to take part in the contest. We hope that helps and wish you the best of luck!

Remember, HacktoberFest is all about the celebration of open source. Stick to that principle, and you’ll get the results you could only wish for!

P.S. - Thanks to our contributors!

Massive shout out to our contributors: @ussgarci, @h4r1337, @d0m96, @EmmanuelCoder, @gautier_difolco, @vaishnav_mk1, @NeoLight1010, @abscubix, @JFarayola, @Shahx95 and everyone else for making it possible. You rock! 🤘

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/26/erlis-amicus-usecase.html b/blog/2022/11/26/erlis-amicus-usecase.html index 88f92238bd..d5c09a09e7 100644 --- a/blog/2022/11/26/erlis-amicus-usecase.html +++ b/blog/2022/11/26/erlis-amicus-usecase.html @@ -19,13 +19,13 @@ - - + +
-

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

· 5 min read
Matija Sosic

amicus hero shot

Erlis Kllogjri is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out.

Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.

Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!

Looking for a full-stack “all-in-one” solution, with React & Node.js

Erlis first learned about Wasp on HackerNews and it immediately caught his attention, particularly the configuration language part. One of the companies he worked at in the past had its own internal DSL in the hardware domain, and he understood how helpful it could be for moving fast and avoiding boilerplate.

Erlis also had previous experience in web development, especially on the front-end side in React and Javascript, so that made Wasp a logical choice.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

— Erlis Kllogjri - Amicus

Building Amicus v1.0 and getting first customers!

The idea for Amicus came from his brother, who is employed at a law firm - talking about their process and challenges in executing them, Erlis thought it would be an interesting side project, especially given there is a real problem to solve.

Soon, the first version of Amicus was live! It was made in a true lean startup fashion, starting with the essential features and immediately being tested with users.

Amicus's dashboard
Amicus's dashboard, using Material-UI

Erlis used Material-UI as a UI library since it came with one of the example apps built in Wasp (Beta introduced Tailwind support!). Users could track their clients, active legal matters and there was even integrated billing with Stripe! Amicus also extensively used Wasp’s Async Jobs feature to regularly update invoices, send reminder emails and clear out old data from the database.

After a few iterations with the legal team who were Amicus' test user (e.g. adding support for different types of users via roles), they were ready to get onboarded and become paying customers! More than 20 people from a single company are using Amicus daily for their work, making it an amazing source of continuous feedback for further development.

Erlis enjoyed the most how fast he could progress and ship features with Wasp on a weekly basis. Having both front-end, back-end, and database set and fully configured to work together from the beginning, he could focus on developing features rather than spend time figuring out the intricacies of the specific stack.

If it weren't for Wasp, Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me.

— Erlis Kllogjri - Amicus

Beyond MVP with Wasp

Although Erlis already has a product running in production, with first paying customers, he wants to see how far he can take it and has a lot of ideas (also requests) for the next features. (Actually, Erlis had a big kanban board with post-its on a wall behind him as we were chatting, dedicated just to Amicus - that was impressive to see!).

Some of the most imminent ones are:

  • uploading and sharing files between lawyers and clients
  • usage logging and analytics
  • transactional emails for notifications

Since under the hood Wasp is generating code in today's mainstream, production-tested technologies such as React, Node.js and PostgreSQL (through Prisma), there aren't any technical limitations to scaling Amicus as it grows and attracts more users.

Also, given that the wasp build CLI command generates a ready Docker image for the back-end (and static files for the front-end), deployment options are unlimited. Since Heroku is shutting down its free plan, we added guides on how to deploy your project for free on Fly.io and Railway (freemium).

I was using Wasp while still in Alpha and was impressed how well everything worked, especially given how much stuff I get. I had just a few minor issues and the team responded super quickly on Discord and helped me resolve it.

— Erlis Kllogjri - Amicus

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

· 5 min read
Matija Sosic

amicus hero shot

Erlis Kllogjri is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out.

Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.

Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!

Looking for a full-stack “all-in-one” solution, with React & Node.js

Erlis first learned about Wasp on HackerNews and it immediately caught his attention, particularly the configuration language part. One of the companies he worked at in the past had its own internal DSL in the hardware domain, and he understood how helpful it could be for moving fast and avoiding boilerplate.

Erlis also had previous experience in web development, especially on the front-end side in React and Javascript, so that made Wasp a logical choice.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

— Erlis Kllogjri - Amicus

Building Amicus v1.0 and getting first customers!

The idea for Amicus came from his brother, who is employed at a law firm - talking about their process and challenges in executing them, Erlis thought it would be an interesting side project, especially given there is a real problem to solve.

Soon, the first version of Amicus was live! It was made in a true lean startup fashion, starting with the essential features and immediately being tested with users.

Amicus's dashboard
Amicus's dashboard, using Material-UI

Erlis used Material-UI as a UI library since it came with one of the example apps built in Wasp (Beta introduced Tailwind support!). Users could track their clients, active legal matters and there was even integrated billing with Stripe! Amicus also extensively used Wasp’s Async Jobs feature to regularly update invoices, send reminder emails and clear out old data from the database.

After a few iterations with the legal team who were Amicus' test user (e.g. adding support for different types of users via roles), they were ready to get onboarded and become paying customers! More than 20 people from a single company are using Amicus daily for their work, making it an amazing source of continuous feedback for further development.

Erlis enjoyed the most how fast he could progress and ship features with Wasp on a weekly basis. Having both front-end, back-end, and database set and fully configured to work together from the beginning, he could focus on developing features rather than spend time figuring out the intricacies of the specific stack.

If it weren't for Wasp, Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me.

— Erlis Kllogjri - Amicus

Beyond MVP with Wasp

Although Erlis already has a product running in production, with first paying customers, he wants to see how far he can take it and has a lot of ideas (also requests) for the next features. (Actually, Erlis had a big kanban board with post-its on a wall behind him as we were chatting, dedicated just to Amicus - that was impressive to see!).

Some of the most imminent ones are:

  • uploading and sharing files between lawyers and clients
  • usage logging and analytics
  • transactional emails for notifications

Since under the hood Wasp is generating code in today's mainstream, production-tested technologies such as React, Node.js and PostgreSQL (through Prisma), there aren't any technical limitations to scaling Amicus as it grows and attracts more users.

Also, given that the wasp build CLI command generates a ready Docker image for the back-end (and static files for the front-end), deployment options are unlimited. Since Heroku is shutting down its free plan, we added guides on how to deploy your project for free on Fly.io and Railway (freemium).

I was using Wasp while still in Alpha and was impressed how well everything worked, especially given how much stuff I get. I had just a few minor issues and the team responded super quickly on Discord and helped me resolve it.

— Erlis Kllogjri - Amicus

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/26/michael-curry-usecase.html b/blog/2022/11/26/michael-curry-usecase.html index 9554abfd2e..5bf254bf28 100644 --- a/blog/2022/11/26/michael-curry-usecase.html +++ b/blog/2022/11/26/michael-curry-usecase.html @@ -19,13 +19,13 @@ - - + +
-

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

· 5 min read
Matija Sosic

grabbit hero shot

Michael Curry is a senior front-end engineer at Improbable, a metaverse and simulation company based in London. In his free time he enjoys learning about compilers.

In his previous position at StudentBeans, he experienced the problem of multiple engineering teams competing for the same dev environment (e.g. testing, staging, …). Then he discovered Wasp and decided to do something about it!

Read on to learn why Michael chose Wasp to build and deploy an internal tool for managing development environments at StudentBeans.

The problem: the battle for the dev environment

StudentBeans has a microservices-based architecture with multiple environments - test, staging, production, …. The team practices CI/CD and deploys multiple times a day. With such a rapid development speed, it would relatively often happen that multiple engineering teams attempt to claim the same dev environment at the same time.

There wasn't an easy way for teams to synchronize on who is using which environment and it would eventually lead to unexpected changes, confusion, and prolonged development times.

The solution: Grabbit - claim and release dev environments as-you-go

After the incident described above repeated for the n-th time, the team got together for a postmortem. They decided their new development process should look like this:

  • merge your changes
  • claim the environment you want to deploy to (e.g. testing, staging, …)
  • deploy your changes
  • test your changes
  • release the environment once you are done with it so others are able to claim it

The other requirements were to build the solution in-house to save money and also not to spend more than a few hours on it as they still needed to deliver some important features for the ongoing sprint.

The power of rapid prototyping with Wasp

Michael learned about Wasp during its first HackerNews launch and it immediately caught his eye. Being a programming language enthusiast himself, he immediately understood the value of a DSL approach and how it could drastically simplify the development process, while at the same time not preventing him from using his preferred tech stack (React, Node.js) when needed.

Also, although Michael had full-stack experience, his primary strength at the time was on the front-end side. Wasp looked like a great way of not having to deal with the tedious back-end setup and wiring (setting up the database, figuring out API, …) and being able to focus on the UX.

When I first learned about Wasp on HN I was really excited about its DSL approach. It was amazing how fast I could get things running with Wasp - I had the first version within an hour! The language is also fairly simple and straightforward and plays well with React & Node.js + it removes a ton of boilerplate.

— Michael Curry - Grabbit

Out-of-the-box deployment

Once Michael was satisfied with the first version of Grabbit, and confirmed with the team it fits their desired process, the only thing left to do was to deploy it! It is well known this step can get really complicated, especially if you're not yet well-versed in the sea of config options that usually come with it.

Wasp CLI comes with a wasp build command that does all the heavy lifting for you - it creates a directory with static front-end files that you can easily deploy to e.g. Netlify, and on the other hand, a Docker image for the back-end. Since Heroku is ending its free plan, our recommendation is to deploy to Fly.io, for which the detailed guide is provided. You can find the detailed deployment instructions here.

In Michael's case, he deployed Grabbit behind the VPN since it was an internal tool, and this process was made easy by having a ready-to-go Dockerfile.

From MVP to a full-fledged SaaS without a rewrite

The presented functionality of Grabbit above is quite simple (create a resource → claim it → release it), and it could have easily been implemented in some no-code tool or, if we really wanted to go simple, with a Trello board. So why use Wasp at all?

One reason is that developers know and prefer their tools and trust code over the no-code solutions, especially when requirements are still evolving and it is not evident they won't get "stuck" in some closed system. Michael had similar thinking - as he identified this problem at his own company, he realized others must be facing the same issue as well. That is why his plan was to keep improving Grabbit and eventually offer it as a standalone SaaS.

This is where Wasp comes in - he could develop and deploy an initial version of Grabbit in a matter of hours, but still end up with a platform that he can extend indefinitely through the power of code with his stack of choice, React & Node.js, while also using the npm packages he is using everyday at work.

Once he starts adding more advanced features, such as multi-user support with authentication, email notifications, and integration with CI/CD, no-code tools won't cut it any more. This way he saved himself and the company from throwing an MVP away and starting everything from scratch (having to learn the new technology and figure out how to set it all up) as the product evolves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

· 5 min read
Matija Sosic

grabbit hero shot

Michael Curry is a senior front-end engineer at Improbable, a metaverse and simulation company based in London. In his free time he enjoys learning about compilers.

In his previous position at StudentBeans, he experienced the problem of multiple engineering teams competing for the same dev environment (e.g. testing, staging, …). Then he discovered Wasp and decided to do something about it!

Read on to learn why Michael chose Wasp to build and deploy an internal tool for managing development environments at StudentBeans.

The problem: the battle for the dev environment

StudentBeans has a microservices-based architecture with multiple environments - test, staging, production, …. The team practices CI/CD and deploys multiple times a day. With such a rapid development speed, it would relatively often happen that multiple engineering teams attempt to claim the same dev environment at the same time.

There wasn't an easy way for teams to synchronize on who is using which environment and it would eventually lead to unexpected changes, confusion, and prolonged development times.

The solution: Grabbit - claim and release dev environments as-you-go

After the incident described above repeated for the n-th time, the team got together for a postmortem. They decided their new development process should look like this:

  • merge your changes
  • claim the environment you want to deploy to (e.g. testing, staging, …)
  • deploy your changes
  • test your changes
  • release the environment once you are done with it so others are able to claim it

The other requirements were to build the solution in-house to save money and also not to spend more than a few hours on it as they still needed to deliver some important features for the ongoing sprint.

The power of rapid prototyping with Wasp

Michael learned about Wasp during its first HackerNews launch and it immediately caught his eye. Being a programming language enthusiast himself, he immediately understood the value of a DSL approach and how it could drastically simplify the development process, while at the same time not preventing him from using his preferred tech stack (React, Node.js) when needed.

Also, although Michael had full-stack experience, his primary strength at the time was on the front-end side. Wasp looked like a great way of not having to deal with the tedious back-end setup and wiring (setting up the database, figuring out API, …) and being able to focus on the UX.

When I first learned about Wasp on HN I was really excited about its DSL approach. It was amazing how fast I could get things running with Wasp - I had the first version within an hour! The language is also fairly simple and straightforward and plays well with React & Node.js + it removes a ton of boilerplate.

— Michael Curry - Grabbit

Out-of-the-box deployment

Once Michael was satisfied with the first version of Grabbit, and confirmed with the team it fits their desired process, the only thing left to do was to deploy it! It is well known this step can get really complicated, especially if you're not yet well-versed in the sea of config options that usually come with it.

Wasp CLI comes with a wasp build command that does all the heavy lifting for you - it creates a directory with static front-end files that you can easily deploy to e.g. Netlify, and on the other hand, a Docker image for the back-end. Since Heroku is ending its free plan, our recommendation is to deploy to Fly.io, for which the detailed guide is provided. You can find the detailed deployment instructions here.

In Michael's case, he deployed Grabbit behind the VPN since it was an internal tool, and this process was made easy by having a ready-to-go Dockerfile.

From MVP to a full-fledged SaaS without a rewrite

The presented functionality of Grabbit above is quite simple (create a resource → claim it → release it), and it could have easily been implemented in some no-code tool or, if we really wanted to go simple, with a Trello board. So why use Wasp at all?

One reason is that developers know and prefer their tools and trust code over the no-code solutions, especially when requirements are still evolving and it is not evident they won't get "stuck" in some closed system. Michael had similar thinking - as he identified this problem at his own company, he realized others must be facing the same issue as well. That is why his plan was to keep improving Grabbit and eventually offer it as a standalone SaaS.

This is where Wasp comes in - he could develop and deploy an initial version of Grabbit in a matter of hours, but still end up with a platform that he can extend indefinitely through the power of code with his stack of choice, React & Node.js, while also using the npm packages he is using everyday at work.

Once he starts adding more advanced features, such as multi-user support with authentication, email notifications, and integration with CI/CD, no-code tools won't cut it any more. This way he saved himself and the company from throwing an MVP away and starting everything from scratch (having to learn the new technology and figure out how to set it all up) as the product evolves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/26/wasp-beta-launch-week.html b/blog/2022/11/26/wasp-beta-launch-week.html index 5755afc40f..4f5f595e7e 100644 --- a/blog/2022/11/26/wasp-beta-launch-week.html +++ b/blog/2022/11/26/wasp-beta-launch-week.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Beta Launch Week announcement

· 5 min read
Matija Sosic

It’s almost here! After almost two years since our Alpha release, countless apps developed, React and Node versions upgraded, and PRs merged we’re only a day away from Beta!

Beta is coming

We’re going to follow a launch week format, which means our Beta launch will last for the whole week! Starting with the Product Hunt launch this Sunday (we’ll let you know once we’re live, so sharpen your upvoting fingers!) we’ll highlight a new feature every day.

I’ll try not to spoil too much in advance but we’re really excited about this - here follows a quick overview of what it’s gonna look like:

Sunday, Nov 27 - Product Hunt launch event 🚀 + let’s get this party started: Auth 🎉

Besides defending our Product Hunt title (we won #1 Product of the Day last time), this time we’ll also have an online party for all of us to celebrate together!

It will be held on our Discord at 9:00 am EST / 15:00 CET - sign up here and make sure to mark yourself as “Interested”!

Join us to meet the team, attend a relaxed AMA session to learn everything about Wasp, from how it started to development challenges (having fun with Haskell, web dev and compilers) and ideas and plans for the future.

Beta launch party instructions

The first feature to announce will be authentication in Wasp! It’s easier and cooler than ever, supports 3rd party providers (hint: starts with “G”), and works smoother than a jar of peanut butter (not the crunchy one of course)!

Monday, Nov 28 - TypeScript support!

TypeScript is here!

When we asked you what was missing in Wasp during our Alpha Testing Program, you were pretty clear:

TypeScript is wanted!

We heard you (honestly we were missing it too) and now it’s here! You can write your code in TypeScript and enjoy all the goodies that types bring. Some things already work really well and there are a few for which we still have ideas on how to make them better, but more on that on Tuesday!

Wednesday, Nov 29 - Tailwind support! 🐈 💨

Tailwind Nic Cage

It’s beautiful! Another highly anticipated featured that also comes with Beta - support for Tailwind CSS framework! Since it has an additional build step it didn’t work out-of-the-box with Alpha, but now it works like a breeze (see what I did here?)!

Honestly, having used it for designing our new Beta landing page I can really see why it gained so much popularity. So long, making up names for classes, “containers”, and “wrappers”!

Thursday, Nov 30 - Optimistic updates!

Without optimistic updates
Stop glitching, dang it!

You know that feeling when you move your Trello card “Try Wasp Beta” from “Todo” column to “Done” column and everything works super smoothly without any glitches? That’s because of optimistic updates! You may not need it often but if you needed and it wasn’t possible you’d feel really sad.

Well, that’s why Alpha is called Alpha and Beta is called Beta 😅. Long story short, now it’s possible to do it in Wasp and it’s also super easy and clean! We're actually very optimistic you’ll feel really good about implementing optimistic updates for your app in Wasp.

Friday, Dec 1 - Improved IDE support, tooling and Wasp LSP!

VS Code support for Wasp LSP

If you like types in TypeScript (and in general), then you will also enjoy Wasp! Our DSL is also a typed language which means it can report errors in compile time, e.g. in case you haven’t configured your route correctly. And now all that happens directly in your editor!

Beta brings LSP, Language Server for Wasp that works with VS Code (support for other editors coming soon! I’m VIM user myself so take a guess :D). That means improved syntax highlighting, code autocompletion and live error reporting - everything you’d expect from a language!

Wasp Language Server in action
Wasp LSP in action!

Saturday, Dec 2 - Grande Finale + #1 Wasp Hackathon!(Waspathon🐝 ?)

First Wasp hackathon

I don’t want to reveal too much in advance, but yep there will be a hackathon, yep there will be cool rewards (at least we think so) and yep it will be awesome! We’ll officially announce it as we end the launch week, and equipped with all the new features Beta brought we’ll switch into the hacking mode!

It’s our first hackathon and we can’t wait to tell you more about it (ok, I admit, we’re still working on it) and see what you beeld with Wasp!

Recap

  • We are launching Beta this Sunday, Nov 27, on Product Hunt at 1am PST / 4am EST / 10am CET - make sure to upvote and comment (anything counts, even “go guys!”) when you can
  • Beta brings a ton of new exciting features - we’ll highlight one each day of the following week
  • On Saturday, Dec 2, we’ll announce a hackathon - our first ever!

That’s it, Waspeteers - keep buzzing as always and see you soon on the other side! 🐝  🅱️

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Beta Launch Week announcement

· 5 min read
Matija Sosic

It’s almost here! After almost two years since our Alpha release, countless apps developed, React and Node versions upgraded, and PRs merged we’re only a day away from Beta!

Beta is coming

We’re going to follow a launch week format, which means our Beta launch will last for the whole week! Starting with the Product Hunt launch this Sunday (we’ll let you know once we’re live, so sharpen your upvoting fingers!) we’ll highlight a new feature every day.

I’ll try not to spoil too much in advance but we’re really excited about this - here follows a quick overview of what it’s gonna look like:

Sunday, Nov 27 - Product Hunt launch event 🚀 + let’s get this party started: Auth 🎉

Besides defending our Product Hunt title (we won #1 Product of the Day last time), this time we’ll also have an online party for all of us to celebrate together!

It will be held on our Discord at 9:00 am EST / 15:00 CET - sign up here and make sure to mark yourself as “Interested”!

Join us to meet the team, attend a relaxed AMA session to learn everything about Wasp, from how it started to development challenges (having fun with Haskell, web dev and compilers) and ideas and plans for the future.

Beta launch party instructions

The first feature to announce will be authentication in Wasp! It’s easier and cooler than ever, supports 3rd party providers (hint: starts with “G”), and works smoother than a jar of peanut butter (not the crunchy one of course)!

Monday, Nov 28 - TypeScript support!

TypeScript is here!

When we asked you what was missing in Wasp during our Alpha Testing Program, you were pretty clear:

TypeScript is wanted!

We heard you (honestly we were missing it too) and now it’s here! You can write your code in TypeScript and enjoy all the goodies that types bring. Some things already work really well and there are a few for which we still have ideas on how to make them better, but more on that on Tuesday!

Wednesday, Nov 29 - Tailwind support! 🐈 💨

Tailwind Nic Cage

It’s beautiful! Another highly anticipated featured that also comes with Beta - support for Tailwind CSS framework! Since it has an additional build step it didn’t work out-of-the-box with Alpha, but now it works like a breeze (see what I did here?)!

Honestly, having used it for designing our new Beta landing page I can really see why it gained so much popularity. So long, making up names for classes, “containers”, and “wrappers”!

Thursday, Nov 30 - Optimistic updates!

Without optimistic updates
Stop glitching, dang it!

You know that feeling when you move your Trello card “Try Wasp Beta” from “Todo” column to “Done” column and everything works super smoothly without any glitches? That’s because of optimistic updates! You may not need it often but if you needed and it wasn’t possible you’d feel really sad.

Well, that’s why Alpha is called Alpha and Beta is called Beta 😅. Long story short, now it’s possible to do it in Wasp and it’s also super easy and clean! We're actually very optimistic you’ll feel really good about implementing optimistic updates for your app in Wasp.

Friday, Dec 1 - Improved IDE support, tooling and Wasp LSP!

VS Code support for Wasp LSP

If you like types in TypeScript (and in general), then you will also enjoy Wasp! Our DSL is also a typed language which means it can report errors in compile time, e.g. in case you haven’t configured your route correctly. And now all that happens directly in your editor!

Beta brings LSP, Language Server for Wasp that works with VS Code (support for other editors coming soon! I’m VIM user myself so take a guess :D). That means improved syntax highlighting, code autocompletion and live error reporting - everything you’d expect from a language!

Wasp Language Server in action
Wasp LSP in action!

Saturday, Dec 2 - Grande Finale + #1 Wasp Hackathon!(Waspathon🐝 ?)

First Wasp hackathon

I don’t want to reveal too much in advance, but yep there will be a hackathon, yep there will be cool rewards (at least we think so) and yep it will be awesome! We’ll officially announce it as we end the launch week, and equipped with all the new features Beta brought we’ll switch into the hacking mode!

It’s our first hackathon and we can’t wait to tell you more about it (ok, I admit, we’re still working on it) and see what you beeld with Wasp!

Recap

  • We are launching Beta this Sunday, Nov 27, on Product Hunt at 1am PST / 4am EST / 10am CET - make sure to upvote and comment (anything counts, even “go guys!”) when you can
  • Beta brings a ton of new exciting features - we’ll highlight one each day of the following week
  • On Saturday, Dec 2, we’ll announce a hackathon - our first ever!

That’s it, Waspeteers - keep buzzing as always and see you soon on the other side! 🐝  🅱️

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/28/why-we-chose-prisma.html b/blog/2022/11/28/why-we-chose-prisma.html index 5f2ba37c2b..c20825abde 100644 --- a/blog/2022/11/28/why-we-chose-prisma.html +++ b/blog/2022/11/28/why-we-chose-prisma.html @@ -19,13 +19,13 @@ - - + +
-

Why we chose Prisma as a database layer for Wasp

· 7 min read
Martin Sosic

Beta is coming

Wasp is a full-stack JS web dev framework, covering frontend, backend, and database. When choosing the solution to build our database layer on top, we chose Prisma, even though it was still somehwat new tech at that point, and we believe today we made a great choice -> read on to learn why!

At Wasp, we aim to simplify full-stack web development via a specialized high-level language. This language allows you to describe the main parts of your web app succinctly, avoiding a lot of usual boilerplate and configuration while giving you lots of features and ensuring best practices. Wasp is essentially a full-stack web framework implemented as a specialized language that works with React & Node.js!

When we started working on Wasp, we wanted to keep it easy to learn and to the point, so we decided:

  • the Wasp language should only be used at a high level, so you would still use React, NodeJS, HTML, CSS, etc. to implement your custom logic. If a full-stack web app is an orchestra, Wasp is the conductor.
  • the Wasp language should be declarative and simple, very similar to JSON, but “smarter” in the sense it understands web app concepts and makes sure your app follows them.

With that in mind, we focused on identifying high-level web app concepts that are worth capturing in the Wasp language. We identified the following parts of a web app:

  • General app info (title, head, favicon, …)
  • Pages and Routes
  • Data Models (aka Entities), e.g. User, Task, Organization, Article, … .
  • Operations (communication between client and server; CRUD on data models, 3rd party APIs, …)
  • Deployment

Entities

Of all of those, Entities are in the middle of everything, present through the whole codebase, and are central to all the other parts of the web app: client, server, and database. They were, however, also the most daunting part to implement!

When we started, we imagined an Entity would look something like this in Wasp:

entity User {
id: Id,
username: String @unique,
email: String @unique
groups: [Group]
}

While adding this initial syntax to our language was feasible, there were also much bigger tasks to tackle in order to make this a proper solution:

  • expand syntax to be flexible enough for real-life use cases
  • support migrations (data and schema)
  • generate code that users can call from JS/TS to query and update entities in the DB
  • and probably a lot of other things that we hadn’t even thought of yet!

Mongoose, Sequelize, … or Prisma?

We already decided that we would pick an ORM(ish) solution for JS/TS which we would build the rest of the features on top of. We started evaluating different ones: Mongoose, Sequelize, TypeORM, … .

But then we looked at Prisma, and the winner was clear! Not only was Prisma taking care of everything that we cared about, but it had one additional feature that made it a perfect fit:

model User {
id Int @id @default(autoincrement())
username String @unique
password String
}

No, this is not another idea of how the syntax for Entities could look like in Wasp language → this is the Prisma Schema Language (PSL)!!!

Prisma Schema Language (PSL)

Indeed, Prisma is unique in having a special, declarative language for describing data models (schema), and it was exactly what we needed for Wasp.

So instead of implementing our own syntax for describing Entities, we decided to use Prisma and their PSL to describe Entities (data models) inside the Wasp language.

Today, Entities are described like this in Wasp language:

// ... some Wasp code ...

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

// ... some Wasp code ...

So in the middle of Wasp, you just switch to writing PSL (Prisma Schema Language) to describe an entity!

Another great thing is that the PSL is at its core a pretty simple language, so we implemented our own parser for it → that means that Wasp actually understands what you wrote, even though it is PSL, and can fully work with it. So we lost nothing by using PSL instead of our own syntax and instead gained all the features that Prisma brings.

Other Benefits

Besides PSL, there were plenty of other reasons why we felt Prisma is a great fit for us:

  • It is targeting Javascript / Typescript.
  • It takes care of migrations and has a nice workflow for doing it.
  • It supports different databases: Mongo, PostgreSQL, CockroachDB, …, which is very important for Wasp since our vision is to support different stacks in the future.
  • It has Prisma Studio - UI for inspecting your database, which we also make available to you via Wasp CLI.
  • It keeps improving quickly and is very focused on a nice developer experience, which is also our focus here at Wasp.
  • Community is extremely welcoming and the core team is super helpful - all of our questions and issues were answered super quickly!

Challenges

While integrating Prisma into Wasp went really smoothly, there were a few hiccups:

  • Getting Prisma CLI to provide interactive output while being called programmatically by Wasp was tricky, and in the end, we had to use a bit of a dirty approach to trick the Prisma CLI into thinking it is called interactively. We opened an issue for this with Prisma, so hopefully, we will be able to remove this once it is resolved: https://github.com/prisma/prisma/issues/7113.
  • In the early days, there were some bugs, however, they were always quickly solved, so updating to the newest Prisma version was often the solution.
  • It took us a bit of fiddling to get Prisma to work with its schema outside of the server’s root directory, but we did get it working in the end!

Most of these were due to us stretching the boundaries of how Prisma was imagined to be used, but in total Prisma proved to be fairly flexible!

Summary

With its declarative language for describing schema, focus on ergonomics, and JS/TS as the target language, Prisma was really a stroke of luck for us - if not for it, it would have taken much more effort to get the Entities working in Wasp.

When we started using it, Prisma was still somewhat early, and it was certainly the least-mature technology in our stack - but we decided to bet on it because it was just a perfect fit, and it made so much sense. Today, with Prisma being a mature and popular solution, we are more than happy we made that choice!

Future

Already, Prisma is playing a big role at Wasp, but there is still more that we plan and want to do:

  • support Prisma’s Enum and Type declarations
  • expose more of Prisma’s CLI commands, especially database seeding
  • add support in Wasp for multiple databases (which Prisma already supports)
  • improve IDE support for PSL within the Wasp language

If you are interested in helping with any of these, reach out to us on this issue https://github.com/wasp-lang/wasp/issues/641, or in any case, join us on our Discord server!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Why we chose Prisma as a database layer for Wasp

· 7 min read
Martin Sosic

Beta is coming

Wasp is a full-stack JS web dev framework, covering frontend, backend, and database. When choosing the solution to build our database layer on top, we chose Prisma, even though it was still somehwat new tech at that point, and we believe today we made a great choice -> read on to learn why!

At Wasp, we aim to simplify full-stack web development via a specialized high-level language. This language allows you to describe the main parts of your web app succinctly, avoiding a lot of usual boilerplate and configuration while giving you lots of features and ensuring best practices. Wasp is essentially a full-stack web framework implemented as a specialized language that works with React & Node.js!

When we started working on Wasp, we wanted to keep it easy to learn and to the point, so we decided:

  • the Wasp language should only be used at a high level, so you would still use React, NodeJS, HTML, CSS, etc. to implement your custom logic. If a full-stack web app is an orchestra, Wasp is the conductor.
  • the Wasp language should be declarative and simple, very similar to JSON, but “smarter” in the sense it understands web app concepts and makes sure your app follows them.

With that in mind, we focused on identifying high-level web app concepts that are worth capturing in the Wasp language. We identified the following parts of a web app:

  • General app info (title, head, favicon, …)
  • Pages and Routes
  • Data Models (aka Entities), e.g. User, Task, Organization, Article, … .
  • Operations (communication between client and server; CRUD on data models, 3rd party APIs, …)
  • Deployment

Entities

Of all of those, Entities are in the middle of everything, present through the whole codebase, and are central to all the other parts of the web app: client, server, and database. They were, however, also the most daunting part to implement!

When we started, we imagined an Entity would look something like this in Wasp:

entity User {
id: Id,
username: String @unique,
email: String @unique
groups: [Group]
}

While adding this initial syntax to our language was feasible, there were also much bigger tasks to tackle in order to make this a proper solution:

  • expand syntax to be flexible enough for real-life use cases
  • support migrations (data and schema)
  • generate code that users can call from JS/TS to query and update entities in the DB
  • and probably a lot of other things that we hadn’t even thought of yet!

Mongoose, Sequelize, … or Prisma?

We already decided that we would pick an ORM(ish) solution for JS/TS which we would build the rest of the features on top of. We started evaluating different ones: Mongoose, Sequelize, TypeORM, … .

But then we looked at Prisma, and the winner was clear! Not only was Prisma taking care of everything that we cared about, but it had one additional feature that made it a perfect fit:

model User {
id Int @id @default(autoincrement())
username String @unique
password String
}

No, this is not another idea of how the syntax for Entities could look like in Wasp language → this is the Prisma Schema Language (PSL)!!!

Prisma Schema Language (PSL)

Indeed, Prisma is unique in having a special, declarative language for describing data models (schema), and it was exactly what we needed for Wasp.

So instead of implementing our own syntax for describing Entities, we decided to use Prisma and their PSL to describe Entities (data models) inside the Wasp language.

Today, Entities are described like this in Wasp language:

// ... some Wasp code ...

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

// ... some Wasp code ...

So in the middle of Wasp, you just switch to writing PSL (Prisma Schema Language) to describe an entity!

Another great thing is that the PSL is at its core a pretty simple language, so we implemented our own parser for it → that means that Wasp actually understands what you wrote, even though it is PSL, and can fully work with it. So we lost nothing by using PSL instead of our own syntax and instead gained all the features that Prisma brings.

Other Benefits

Besides PSL, there were plenty of other reasons why we felt Prisma is a great fit for us:

  • It is targeting Javascript / Typescript.
  • It takes care of migrations and has a nice workflow for doing it.
  • It supports different databases: Mongo, PostgreSQL, CockroachDB, …, which is very important for Wasp since our vision is to support different stacks in the future.
  • It has Prisma Studio - UI for inspecting your database, which we also make available to you via Wasp CLI.
  • It keeps improving quickly and is very focused on a nice developer experience, which is also our focus here at Wasp.
  • Community is extremely welcoming and the core team is super helpful - all of our questions and issues were answered super quickly!

Challenges

While integrating Prisma into Wasp went really smoothly, there were a few hiccups:

  • Getting Prisma CLI to provide interactive output while being called programmatically by Wasp was tricky, and in the end, we had to use a bit of a dirty approach to trick the Prisma CLI into thinking it is called interactively. We opened an issue for this with Prisma, so hopefully, we will be able to remove this once it is resolved: https://github.com/prisma/prisma/issues/7113.
  • In the early days, there were some bugs, however, they were always quickly solved, so updating to the newest Prisma version was often the solution.
  • It took us a bit of fiddling to get Prisma to work with its schema outside of the server’s root directory, but we did get it working in the end!

Most of these were due to us stretching the boundaries of how Prisma was imagined to be used, but in total Prisma proved to be fairly flexible!

Summary

With its declarative language for describing schema, focus on ergonomics, and JS/TS as the target language, Prisma was really a stroke of luck for us - if not for it, it would have taken much more effort to get the Entities working in Wasp.

When we started using it, Prisma was still somewhat early, and it was certainly the least-mature technology in our stack - but we decided to bet on it because it was just a perfect fit, and it made so much sense. Today, with Prisma being a mature and popular solution, we are more than happy we made that choice!

Future

Already, Prisma is playing a big role at Wasp, but there is still more that we plan and want to do:

  • support Prisma’s Enum and Type declarations
  • expose more of Prisma’s CLI commands, especially database seeding
  • add support in Wasp for multiple databases (which Prisma already supports)
  • improve IDE support for PSL within the Wasp language

If you are interested in helping with any of these, reach out to us on this issue https://github.com/wasp-lang/wasp/issues/641, or in any case, join us on our Discord server!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/29/permissions-in-web-apps.html b/blog/2022/11/29/permissions-in-web-apps.html index c9835def16..bc2b004f62 100644 --- a/blog/2022/11/29/permissions-in-web-apps.html +++ b/blog/2022/11/29/permissions-in-web-apps.html @@ -19,12 +19,12 @@ - - + +
-

Permissions (access control) in web apps

· 19 min read
Martin Sosic

At Wasp, we are working on a config language / DSL for building web apps that integrates with React & Node.js.
+

Permissions (access control) in web apps

· 19 min read
Martin Sosic

At Wasp, we are working on a config language / DSL for building web apps that integrates with React & Node.js.
This requires us to deeply understand different parts of what constitutes a web app, in order to be able to model them in our DSL.

Recently our focus was on access control, and I decided to capture the learnings in this blog post, to help others quickly get up to speed on how to do access control in web apps.
So, if you are new to access control in web apps, or have been doing it for some time but want to get a better idea of standard practices, read along!

Quick overview of what this blog post covers:

  1. Permissions, yay! Wait, what are they though? (quick overview of basic terms)
  2. Where do we check permissions in a web app: frontend vs backend vs db
  3. Common approaches (RBAC, ABAC, …)
  4. OWASP recommendations
  5. Implementing access control in practice
  6. Summary (TLDR)

1. Permissions, yay! Wait, what are they though?

Unless your web app is mostly about static content or is a form of art, it will likely have a notion of users and user accounts.

Artistic dolphin painting with brush
This dolphin doesn't need users

In such a case, you will need to know which user has permissions to do what -> who can access which resources, and who can execute which operations.

Some common examples of permissions in action:

  1. User can access only their own user account.
  2. If the user is an admin, they can ban other users’ accounts.
  3. User can read other users’ articles, but can't modify them.
  4. The title and description of the article behind the paywall are publicly accessible, but the content is not.
  5. User can send an email invitation to up to 10 future users per day.

Aha, you mean access control! Sorry, authorization! Hmm, authentication?

There are different terms out there (authentication, authorization, access control, permissions) that are often confused for each other, so let's quickly clarify what each one of them stands for.

Spidermen representing authN, authZ, AC and permissions pointing at each other
They all look the same!

1) Authentication (or as cool kids would say: authN)

Act of verifying the user's identity.
Answers the question "Who are they?"

A: Knock Knock
@@ -45,7 +45,7 @@ An interesting finding is that even though the sample is pretty small, it is clear that devs prefer RBAC over OWASP-recommended ABAC.
I believe this is due to 2 main reasons: RBAC is simpler + there are more libraries/frameworks out there supporting RBAC than ABAC (again, due to it being simpler).
It does seem that ABAC is picking up recently though, so it would be interesting to repeat this poll in the future and see what changes.

Organic development

Organic growth of my code (meme)

Often, we add permission checks to our web app one by one, as needed. For example, if we are using NodeJS with ExpressJS for our server and writing middleware that handles HTTP API requests, we will add a bit of logic into that middleware that does some checks to ensure a user can actually perform that action. Or maybe we will embed “checks” into our database queries so that we query only what the user is allowed to access. Often a combination.

What can be dangerous with such an organic approach is the complexity that arises as the codebase grows - if we don’t put enough effort into centralizing and structuring our access control logic, it can become very hard to reason about it and to do consistent updates to it, leading to mistakes and vulnerabilities.

Imagine having to modify the web app so that user can now only read their own articles and articles of their friends, while before they were allowed to read any article. If there is only one place where we can make this update, we will have a nice time, but if there are a bunch of places and we need to hunt those down first and then make sure they are all updated in the same way, we are in for a lot of trouble and lot of space to make mistakes.

Using an existing solution

Instead of figuring out on our own how to structure the access control code, often it is a better choice to use an existing access control solution! Besides not having to figure and implement everything on your own, another big advantage is that these solutions are battle-tested, which is very important for the code dealing with the security of your web app.

We can roughly divide these solutions into frameworks and (external) providers, where frameworks are embedded into your web app and shipped together with it, while providers are externally hosted and usually paid services.

A couple of popular solutions:

  1. https://casbin.org/ (multiple approaches, multiple languages, provider)
    1. Open source authZ library that has support for many access control models (ACL, RBAC, ABAC, …) and many languages (Go, Java, Node.js, JS, Rust, …). While somewhat complex, it is also powerful and flexible. They also have their Casdoor platform, which is authN and authZ provider.
  2. https://casl.js.org/v5/en/ (ABAC, Javascript)
    1. Open source JS/TS library for ABAC. CASL gives you a nice way to define the ABAC rules in your web / NodeJS code, and then also check them and call them. It has a bunch of integrations with popular solutions like React, Angular, Prisma, Mongoose, … .
  3. https://github.com/CanCanCommunity/cancancan (Ruby on Rails ABAC)
    1. Same like casl.js, but for Ruby on Rails! Casl.js was actually inspired and modeled by cancancan.
  4. https://github.com/varvet/pundit
    1. Popular open-source Ruby library focused around the notion of policies, giving you the freedom to implement your own approach based on that.
  5. https://spring.io/projects/spring-security
    1. Open source authN and authZ framework for Spring (Java).
  6. https://github.com/dfunckt/django-rules
    1. A generic, approachable open source framework for building rule-based systems in Django (Python).
  7. Auth0 (provider)
    1. Auth0 has been around for some time and is probably the most popular authN provider out there. While authN is their main offering (they give you SDKs for authentication + they store user profiles and let you manage them through their SaaS), they also allow you to define authZ to some degree, via RBAC and policies.
  8. https://www.osohq.com/ (provider, DSL)
    1. OSO is an authZ provider, unique in a way that they have a specialized language for authorization (DSL, called Polar) in which you define your authorization rules. They come with support for common approaches (e.g. RBAC, ABAC, ReBAC) but also support custom ones. Then, you can use their open source library embedded in your application, or use their managed cloud offering.
  9. https://warrant.dev/ (Provider)
    1. Relatively new authZ provider, they have a dashboard where you can manage your rules in a central location and then use them from multiple languages via their SDKs, even on the client to perform UI checks. Rules can also be managed programmatically via SDK.
  10. https://authzed.com/ (Provider)
    1. AuthZed brings a specialized SpiceDB permissions database which they use as a centralized place for storing and managing rules. Then, you can use their SDKs to query, store, and validate application permissions.

Summary (TLDR)

  • Authentication (authN) answers “who are they”, authorization (authZ) answers “are they allowed to”, while access control is the overarching term for the whole process of performing authN and authZ.
  • Doing access control on the frontend is just for show (for improving UX) and you can’t rely on it. Any and all real access control needs to be done on the server (possibly a bit in the db, but normally not needed).
  • While it is ok to start with a simple access control approach at the beginning, you should be ready to switch to a more advanced approach once the complexity grows. The most popular approaches for doing access control are RBAC (role-based) and ABAC (attribute-based). RBAC is easier to get going with, but ABAC is more powerful.
  • You should make sure your access control has as little duplication as possible and is centralized, in order to reduce the chance of introducing bugs.
  • It is usually smart to use existing solutions, like access control frameworks or external providers.

Access control in Wasp

In Wasp, we don’t yet have special support for access control, although we are planning to add it in the future. As it seems at the moment, we will probably go for ABAC, and we would love to provide a way to define access rules both at the Operations level and at Entity (data model) level. Due to Wasp’s mission to provide a highly integrated full-stack experience, we are excited about the possibilities this offers to provide an access control solution that is integrated tightly with the whole web app, through the whole stack!

You can check out our discussion about this in our “Support for Permissions” RFC.

Thanks to the reviewers

Karan Kajla (pro advice on RBAC!), Graham Neray (great general advice + pointed out ReBAC), Dennis Walsh (awesome suggestions how to have article read better), Shayne Czyzewski, Matija Sosic, thank you for taking the time to review this article and make it better! Your suggestions, corrections, and ideas were invaluable.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/29/typescript-feature-announcement.html b/blog/2022/11/29/typescript-feature-announcement.html index 90a0b60b33..4f356f958c 100644 --- a/blog/2022/11/29/typescript-feature-announcement.html +++ b/blog/2022/11/29/typescript-feature-announcement.html @@ -19,16 +19,16 @@ - - + +
-

Feature Announcement - TypeScript Support

· 8 min read
Filip Sodić

Wasp TS support

Prologue

TypeScript doesn't need much introduction at this point, so we'll keep it short! +

Feature Announcement - TypeScript Support

· 8 min read
Filip Sodić

Wasp TS support

Prologue

TypeScript doesn't need much introduction at this point, so we'll keep it short! Wasp finally allows you to write your code in TypeScript (i.e., the most popular web technology after JavaScript) on both the front-end and the back-end.

You can now define and use types in any part of your code, enjoying all benefits of the static type checker. At the time of writing, not all parts of Wasp are typed as well as they could be, but we're working on it! Exposing all Wasp functionalities through informative typed interfaces is one of our top priorities.

Without further ado, let's see how we can use TypeScript with Wasp.

Setting up a TypeScript project in Wasp

Let's start by creating a fresh Wasp project:

wasp new myApp

This will generate a project skeleton in the folder myApp. The project structure is different than before, and there are now several additional generated files that help with IDE and TypeScript support. So let's explain it:

.
├── .gitignore
├── main.wasp # Your wasp code goes here.
├── src
│   ├── client # Your client code (JS/CSS/HTML) goes here.
│   │   ├── Main.css
│   │   ├── MainPage.jsx
│   │   ├── react-app-env.d.ts
│   │   ├── tsconfig.json
│   │   └── waspLogo.png
│   ├── server # Your server code (Node JS) goes here.
│   │   └── tsconfig.json
│   ├── shared # Your shared (runtime independent) code goes here.
│   │   └── tsconfig.json
│   └── .waspignore
└── .wasproot

At this point, we can choose one of three options:

  1. We write our code exclusively in JavaScript.
  2. We write our code exclusively in TypeScript.
  3. We write some parts of our code in JavaScript, and other parts in TypeScript.

Since the third option is a superset of the first two, that's what Wasp currently supports. In other words, regardless of whether you want your entire codebase in one of these languages or you want to mix it up, there's no extra configuration necessary! Simply use the appropriate extension (.ts and .tsx for TypeScript; .js and .jsx for JavaScript), and your IDE and Wasp will know what to do.

To demonstrate this, let's start Wasp and change MainPage.jsx to MainPage.tsx:

wasp start
mv src/client/MainPage.jsx src/client/MainPage.tsx

That's it! Wasp will notice the change and recompile, and your app will continue to work. The only difference is that you can now write TypeScript in MainPage.tsx and get helpful information from your IDE and the static type checker. Try removing an import and see what happens.

The same applies to any file you may want to include in your project. Specify the language you wish to use via the extension, and Wasp will do the rest!

caution

Even if you use TypeScript and have a server file called someFile.ts, you must still import it as if it had the .js extension (i.e., import foo from 'someFile.js'). Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general).

Read more about ES modules in TypeScript here. If you're interested in the discussion and the reasoning behind this, read about it in this GitHub issue.

This does not apply to front-end files. Thanks to Webpack, you don't need to write extensions when working with client-side imports.

Moving existing projects to the new structure (and optionally TypeScript)

If you wish to move an existing project to the new structure, the easiest approach comes down to creating a new project and moving all the files from your old project into appropriate locations. After doing this, you can choose which files you'd like to implement in TypeScript, change the extension and go for it.

To avoid digging too deep, this is all we'll say about migrating. For a more detailed migration guide, check our changelog. It explains everything step-by-step.

TypeScript in action

Finally, let's demonstrate how TypeScript helps us by using it in a small Todo app. The part of our code in charge of rendering tasks looks something like this:


function MainPage() {
const { data: tasks } = useQuery(getTasks)

return (
<div>
<h1>Todos</h1>
<TaskList tasks={tasks} />
</div>
)
}

function TaskList({ tasks }) {
if (!tasks.len) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx}/>)}
</div>
)
}



function Task({ id, isdone, description }) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isdone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

Try to see if you can find any bugs. When you're confident you've got all of them, continue reading.

Let's see what happens when we bring TypeScript into the picture. Remember, we only need to change the extension to tsx. After we do this, The IDE will warn us about missing type definitions, so let's fill these in. While we're at it, we can also tell useQuery what types it's working with by specifying its type arguments.

Here's how our code looks after these changes:

type Task = {
id: string
description: string
isDone: boolean
}

function MainPage() {
const { data: tasks } = useQuery<Task, Task[]>(getTasks)

return (
<div>
<h1>Todos</h1>
<TaskList tasks={tasks} />
</div>
)
}

function TaskList({ tasks }: { tasks: Task[] }) {
if (!tasks.len) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx}/>)}
</div>
)
}



function Task({ id, isdone, description }: Task) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isdone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

As soon as we change our code, TypeScript detects three errors:

TypeScript erros
The errors are pretty simple (almost as if we've made them up for this example :)

  1. The first error warns us that tasks might be undefined (e.g., on the first render), which TaskList does not expect
  2. The second error tells us that the property len does not exist on the array tasks. In other words, we misspelled length.
  3. Finally, the third error tells us that the type Task does not contain the field isdone. This is also a typo. The field's name should be isDone.

Thanks to TypeScript, we can quickly fix all three errors, saving us a lot of time we'd probably lose by hunting them down manually or, even worse, during runtime.


type Task = {
id: string
description: string
isDone: boolean
}
function MainPage() {
const { data: tasks } = useQuery<Task, Task[]>(getTasks)

return (
<div>
<h1>Todos</h1>
{tasks && <TaskList tasks={tasks} />}
</div>
)
}

function TaskList({ tasks }: { tasks: Task[] }) {
if (!tasks.length) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx} />)}
</div>
)
}



function Task({ id, isDone, description }: Task) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isDone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

And that's it! This is the joy of TypeScript. We've easily fixed all reported errors, and our code should now work correctly (well, at least less incorrectly).

Future work

You might have noticed that, if we want to use the Task type, we have to write most of its type definition twice - once when defining the Task entity in the .wasp file and then again in our code. While we can define the type in src/shared to avoid writing (almost) the same code on both the server and the client, we'll still have duplication between the code in src/shared and our .wasp file.

The good news is that we know about this, also find it annoying, and are working to fix it as soon as possible! In the near future, Wasp will generate types from entities and allow you to access them using @wasp imports. Other improvements exist, too. For example, Wasp could read your query declarations and provide you with the correct type for the context object in their definitions. Another possible improvement is automatically typing queries on the front-end, and then relying on type inference to correctly type useQuery (instead of users specifying its type arguments explicitly).

In short, there's a long and exciting path ahead of us, full of interesting possibilities. So stick with Wasp and see how far we can make it!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/29/wasp-beta.html b/blog/2022/11/29/wasp-beta.html index 9c11237070..80d518be8b 100644 --- a/blog/2022/11/29/wasp-beta.html +++ b/blog/2022/11/29/wasp-beta.html @@ -19,14 +19,14 @@ - - + +
-

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

· 3 min read
Matija Sosic

Wasp is a simple configuration language for building full-stack web apps with less code and ensured best practices. It integrates with React, Node.js and Prisma and provides a lot of common features (auth, CRUD, async jobs, ...) out of the box.

Today, we’re moving to Beta.

Since the launch of Wasp Alpha in February 2021, we’ve been fortunate to work with hundreds of early adopters who helped us shape the product and prioritise the features to build. Number of applications have been deployed to production and even the first revenue generating product was built on top of Wasp.

Alpha in numbers

  • 1,011 projects created
  • 2,012 GitHub stars
  • 45 GitHub contributors
  • 243 issues closed
  • 42,170 lines of code

Here are the the new features that ship with Beta:

🟦 TypeScript support

Developers can now write all their code in TypeScript both on client and server. We’re also in the process of migrating our codebase and adding new types to Wasp imports every day.

Learn more here →

🔑 Full-stack authentication

Besides username & password, Wasp now also supports authentication with Google. We offer both UI helpers (forms you can just import) and functions you can call from client or server if you need more control.

Learn more here →

💨 Tailwind support

Tailwind CSS framework is now supported in Wasp. Just add two files to the project and you’re ready to go!

Learn more here →

⏳ Async jobs/workers

Developers can run one-time or schedule repeating functions that run out of the regular request-response band. This is useful for e.g. sending emails, crunching data, generating reports and other resources intensive tasks. Powered by pg-boss, zero setup required.

Learn more here →

🥛 Optimistic updates support

Wasp will by default propagate your data model changes across the stack. Still, in some cases +

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

· 3 min read
Matija Sosic

Wasp is a simple configuration language for building full-stack web apps with less code and ensured best practices. It integrates with React, Node.js and Prisma and provides a lot of common features (auth, CRUD, async jobs, ...) out of the box.

Today, we’re moving to Beta.

Since the launch of Wasp Alpha in February 2021, we’ve been fortunate to work with hundreds of early adopters who helped us shape the product and prioritise the features to build. Number of applications have been deployed to production and even the first revenue generating product was built on top of Wasp.

Alpha in numbers

  • 1,011 projects created
  • 2,012 GitHub stars
  • 45 GitHub contributors
  • 243 issues closed
  • 42,170 lines of code

Here are the the new features that ship with Beta:

🟦 TypeScript support

Developers can now write all their code in TypeScript both on client and server. We’re also in the process of migrating our codebase and adding new types to Wasp imports every day.

Learn more here →

🔑 Full-stack authentication

Besides username & password, Wasp now also supports authentication with Google. We offer both UI helpers (forms you can just import) and functions you can call from client or server if you need more control.

Learn more here →

💨 Tailwind support

Tailwind CSS framework is now supported in Wasp. Just add two files to the project and you’re ready to go!

Learn more here →

⏳ Async jobs/workers

Developers can run one-time or schedule repeating functions that run out of the regular request-response band. This is useful for e.g. sending emails, crunching data, generating reports and other resources intensive tasks. Powered by pg-boss, zero setup required.

Learn more here →

🥛 Optimistic updates support

Wasp will by default propagate your data model changes across the stack. Still, in some cases you might want more control over that flow for the sake of smoother UX - that is now easy to achieve with Wasp.

Learn more here →

📟 Wasp Language Server

Wasp now has its own LSP for VS Code (other editors coming soon) - that means improved syntax highlighting, code snippets, autocompletion, and error reporting.

Learn more here →

What’s next?

The next features are going to be about making Wasp easier to use - more examples, starter templates and UI helpers. Longer term, we’ll look into deeper integration of data models throughout the stack and supporting more functionalities through the DSL.

It’s Beta Launch Week and we’re highlighting a new feature every week. Also, at the end of the week we’ll kick-off first Wasp hackathon! Signup here to stay in the loop.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/30/optimistic-update-feature-announcement.html b/blog/2022/11/30/optimistic-update-feature-announcement.html index 8672c780c6..df04c51d26 100644 --- a/blog/2022/11/30/optimistic-update-feature-announcement.html +++ b/blog/2022/11/30/optimistic-update-feature-announcement.html @@ -19,16 +19,16 @@ - - + +
-

Feature Release Announcement - Wasp Optimistic Updates

· 7 min read
Filip Sodić

We’re excited to announce that Wasp actions now feature native support for optimistic updates! +

Feature Release Announcement - Wasp Optimistic Updates

· 7 min read
Filip Sodić

We’re excited to announce that Wasp actions now feature native support for optimistic updates! Continue reading to to find out what optimistic updates are and how Wasp implements them.

Wasp TS support

What are Optimistic Updates Anyway?

Think about an interactive web app you use daily. It could be almost anything (e.g., Reddit, Youtube, Facebook). It almost certainly features UI elements you can interact with without refreshing the page, such as upvotes on Reddit or likes on Youtube.

All these small actions play out in the same manner. Let's look at Reddit upvotes as an example:

  1. You click on the upvote button
  2. Your browser sends a request to the server to save the upvote
  3. The server saves your upvote to the database and sends a successful response to your browser
  4. Your browser receives the successful response and reflects the change in the UI (i.e., you see your upvote)

The client waits for the server's confirmation before updating the UI because actions can sometimes fail. Well, at least that was the original idea.

These days, many popular websites update their UIs without waiting for servers' responses. Most of the time, everything goes as expected: you click on an upvote, and the server returns a successful response a couple of seconds later (depending on how fast your connection is). Since programmers want their users to have a snappier experience, instead of waiting for a confirmation, they update the UI immediately (as if the action were successful) and then roll back if the server doesn't return a successful response (which rarely happens). This pattern of optimistically updating the UI before receiving the confirmation of success is called, you guessed it, an Optimistic Update.

Most popular modern websites use optimistic updates to some degree. As mentioned, Reddit uses them for upvotes and downvotes, Youtube uses them for likes, and Trello uses them when moving cards between lists.

Optimistic updates are a significant UX improvement, but since they introduce additional state (which can get out of sync with the server), they can be tricky to get right. Then there's also the issue of writing additional code for managing the cache and rolling back the changes if the request ends up failing. Luckily, we're here to help!

Wasp recently added native support for optimistic updates, and the rest of this post demonstrates how to quickly set it up in your Wasp application.

A Wasp Todo App Without Optimistic Updates

To honor the tradition of demonstrating UIs using Todo apps, We'll show you how to improve the UX of toggling an item's status when working with a slow connection. Before looking at our todo app in action, let's see how we've implemented it in Wasp.

These are the relevant declarations in our .wasp file:

main.wasp
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
psl=}

// A query for fetching all tasks.
query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}


// An action for updating the task's status.
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}

This is the query we use to fetch the tasks (together with their statuses):

queries.js
export const getTasks = async (args, context) => {
return context.entities.Task.findMany()
}

Here's the action we use to update a task’s status:

actions.js
export const updateTask = async ({ id, isDone }, context) => {
return context.entities.Task.updateMany({
where: { id },
data: { isDone }
})
}

Finally, this is how our client uses this action to update a task:

MainPage.js
import updateTask from '@wasp/queries'

// ...

function Task({ id, isDone, description }) {
return (
<div className="task">
<label className="description">
<input
type='checkbox' id={id}
checked={isDone}
onChange={
(e) => updateTask({ id, isDone: e.target.checked })
}
/><span>{description}</span></label>
</div>
)
}

Let's first see how updating a task looks when everything works as expected (i.e., we're on a fast connection):

Normal todo list

So far, so good! But what happens when our connection is not as fast?

Todo list with lag

Hmm, this isn't quite as smooth as we'd like it to be. The user has to wait for several seconds before seeing their their changes reflected by the UI.

How can we improve it? Well, of course, we can optimistically update the checkbox!

Performing a Wasp Action Optimistically

To perform the updateTask action optimistically, all we need to do is decorate the calling code on the client:

MainPage.js
import updateTask from '@wasp/queries'

// ...

function Task({ id, isDone, description }) {
const updateTaskOptimistically = useAction(updateTask, {
optimisticUpdates: [{
// Addressing the query we want to update.
getQuerySpecifier: () => [getTasks],
// Telling Wasp how to update the addressed query using the new payload
// and the previously cached data.
updateQuery: ({ id, isDone }, oldTasks) => oldTasks.map(
task => task.id === id ? { ...task, isDone } : task
)
}]
})

return (
<div className="task">
<label className="description">
<input
type='checkbox' id={id}
checked={isDone}
onChange={
(e) => updateTaskOptimistically({ id, isDone: e.target.checked })
}
/><span>{description}</span></label>
</div>
)
}

Those are all the changes we need, the rest of the code (i.e., main.wasp, queries.js and actions.js) remains the same. We won't describe the API in detail, but if you're curious, everything is covered by our official docs.

Finally, let's see how this version of the app looks in action:

Optimistically updated todo list

Our app no longer waits for the server before rendering the changes. Instead, it updates the cache optimistically, continues waiting for the response, and rolls back the changes if the action fails (Wasp internally handles all of this). As previously mentioned, simple changes such as this one rarely fail. Therefore, most of the time, the user enjoys their snappier experience without ever knowing anything special is happening in the background.

What Makes Optimistic Updates Difficult

There's an old software engineering joke you're probably familiar with:

There are only two hard things in Computer Science: cache invalidation and naming things.

Optimistically updating a query involves plenty of meddling with the client-side cache, which is bound to come with a few gotchas. Examples include the answers to questions such as:

  • What happens when an optimistically updated action fails?
  • What happens when the user uses the optimistically updated data in a new action?
  • What happens when the user performs a different action that affects the same cached data as the optimistically updated one?
  • etc.

Notice how Wasp users don't need to know about any of these issues when using our optimistic updates API. They only need to tell Wasp which query they wish to update and how, and Wasp takes care of the rest.

Wasp internally uses React Query, an excellent asynchronous state management library we'll gladly recommend to anyone. While React Query does solve some of these problems and helps with some of the rest, we still had to implement quite a complex mechanism to fully cover all edge cases.

Describing this mechanism, although technically interesting, is beyond the scope of a feature announcement. But stay tuned because in a future blog post, we'll be taking a deep dive into the infrastructure Wasp uses to ensure optimistic updates are performed correctly and consistently.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/12/01/beta-ide-improvements.html b/blog/2022/12/01/beta-ide-improvements.html index f1f31594a8..2feaaf7cb0 100644 --- a/blog/2022/12/01/beta-ide-improvements.html +++ b/blog/2022/12/01/beta-ide-improvements.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Beta brings major IDE improvements

· 6 min read
Martin Sosic

With the Beta release (0.7), Wasp brings its IDE game to a whole new level!

So far Wasp didn’t have much beyond basic syntax highlighting in VSCode, but now it has:

  1. Wasp language server, that brings the following to your .wasp files:
    1. live error reporting in your editor
    2. autocompletion (basic for now)
  2. VSCode Wasp language extension:
    1. snippets (for page, query, action, entity)
    2. improved syntax highlighting for .wasp files
    3. integration with the above-mentioned language server
  3. Support for popular IDEs to fully support Javascript and Typescript files in the Wasp project.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp Language Server

Wasp Language Server (WLS) is the “brain” behind smart IDE features like live error reporting and autocompletion - so if it seems like IDE actually understands your code to some degree, well that is the language server!

tip

For curious, check out the source code of WLS on Github: https://github.com/wasp-lang/wasp/tree/main/waspc/waspls/src/Wasp/LSP .

Features

Live error/warning reporting

WLS compiles wasp code for you as you work on it and shows you any errors directly in the editor, via red squiggly lines.

Autocompletion

WLS understands at which part of code you are right now and offers appropriate completions for it.

note

Right now WLS is pretty naive here, and mostly focuses on offering available expressions when it realizes you need an expression. This is helpful but just a start, and it will get much smarter in future versions!

Bit of history: why are Language Servers cool

Years ago, there was no standardized way to write something like Language Server for your language, instead, each language was doing something of its own, and then each editor/IDE would also implement its own layer of logic for using it, and that was a loooot of work that needed to be done for each editor!

Luckily, Microsoft then came up with Language Server Protocol - a standardized way of communicating between the “smart” part, implemented by language creators, and the editor/IDE part (language extension) that is using it. This enabled each editor to implement this logic for interacting with language servers only once, and then it can be used for any language server!

This is great for us, language creators, because it means that once we implement a language server for our language, most of the work is done, and the work we need to do per each editor is manageable.

Right now WLS is used only by the VSCode Wasp language extension, but thanks to the nature of the Language Server Protocol, it should be relatively easy to add support for other editors too! Check this GH issue if you are interested in helping.

Setup

The best thing: there is nothing you, as a Wasp user, have to do to set up WLS! It already comes bundled with your installation of wasp → so if you can run wasp projects on your machine, you already have WLS, and it is always of the correct version needed for your current wasp installation. The only thing you need to ensure is you have wasp version ≥ 0.6, and a relatively fresh VSCode Wasp language extension.

An easy way to check that your version of wasp has WLS packaged into it is to run it and look at its usage instructions: it should mention waspls as one of the commands.

Wasp VSCode extension

If we would call Wasp Language Server (WLS) the “backend”, then VSCode Wasp language extension would be “frontend” → it takes care of everything to ensure you have a nice experience working with Wasp in VSCode, while delegating the hardest work to the WLS.

tip

For curious, you can check out its source code here, core of it is just one file: https://github.com/wasp-lang/vscode-wasp/blob/main/src/extension.ts

Features

Syntax highlighting

Nothing unexpected here: it recognizes different parts of Wasp syntax, like type, value, identifier, comment, string, … and colors them appropriately.

If you are curious how is this implemented, check https://github.com/wasp-lang/vscode-wasp/blob/main/syntaxes/wasp.tmLanguage.yaml → the whole syntax of Wasp is described via this “mysterious” old TextMate format, since that is the way to do it in VSCode.

Snippets

Wasp allows you to quickly generate a snippet of code for a new page, query, action, or entity!

Check out our snippet definitions here: https://github.com/wasp-lang/vscode-wasp/blob/main/snippets/wasp.json . It is actually really easy, in VSCode, to define them and add new ones.

Live error reporting + autocompletion

This is done by delegating the work to WLS, as described above!

IDE support for Javascript / Typescript in Wasp project

Due to how unique Wasp is in its approach, getting an IDE to provide all the usual features for Javascript / Typescript wasn’t completely working, and instead, the IDE would get somewhat confused with the context in which files are and would for example not be able to offer “go to definition” for some values, or would not know how to follow the import path.

With Wasp Beta this is now resolved! We resolved this by somewhat changing the structure of the Wasp project and also adding tsconfig.json files that provide IDE with the information needed to correctly analyze the JS/TS source files.

To learn more about Typescript support in Wasp Beta, check this blog post!

What does the future hold?

While Wasp Beta greatly improved IDE support for Wasp, there are still quite a few things we want to improve on:

  1. Smarter autocompletion via WLS.
    1. Right now it suggests any expression when you need an expression. In the future, we want it to know exactly what is the type of needed expression, and suggest only expressions of that type! So if I am in route ... { to: <my_cursor_here> }, then I want to see only pages among the suggested completions, not queries or actions or something else.
    2. Further, we would also like it to autocomplete on dictionary fields → so if I am in route ... { <my_cursor_here> }, it should offer me path and to as completions, as those are only valid fields in the route dictionary.
  2. Extensions for other editors besides VSCode. Now that we have Wasp Language Server, these shouldn’t be too hard to implement! This is also a great task for potential contributors: check this GH issue if you are interested.
  3. Implement Wasp code formatter. We could make it a part of WLS, and then have the editor extension call it on save.
  4. Improve support for PSL (Prisma Schema Language) in .wasp files.

If any of these sound interesting, feel free to join us on our Github, or join the discussion on Discord!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Beta brings major IDE improvements

· 6 min read
Martin Sosic

With the Beta release (0.7), Wasp brings its IDE game to a whole new level!

So far Wasp didn’t have much beyond basic syntax highlighting in VSCode, but now it has:

  1. Wasp language server, that brings the following to your .wasp files:
    1. live error reporting in your editor
    2. autocompletion (basic for now)
  2. VSCode Wasp language extension:
    1. snippets (for page, query, action, entity)
    2. improved syntax highlighting for .wasp files
    3. integration with the above-mentioned language server
  3. Support for popular IDEs to fully support Javascript and Typescript files in the Wasp project.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp Language Server

Wasp Language Server (WLS) is the “brain” behind smart IDE features like live error reporting and autocompletion - so if it seems like IDE actually understands your code to some degree, well that is the language server!

tip

For curious, check out the source code of WLS on Github: https://github.com/wasp-lang/wasp/tree/main/waspc/waspls/src/Wasp/LSP .

Features

Live error/warning reporting

WLS compiles wasp code for you as you work on it and shows you any errors directly in the editor, via red squiggly lines.

Autocompletion

WLS understands at which part of code you are right now and offers appropriate completions for it.

note

Right now WLS is pretty naive here, and mostly focuses on offering available expressions when it realizes you need an expression. This is helpful but just a start, and it will get much smarter in future versions!

Bit of history: why are Language Servers cool

Years ago, there was no standardized way to write something like Language Server for your language, instead, each language was doing something of its own, and then each editor/IDE would also implement its own layer of logic for using it, and that was a loooot of work that needed to be done for each editor!

Luckily, Microsoft then came up with Language Server Protocol - a standardized way of communicating between the “smart” part, implemented by language creators, and the editor/IDE part (language extension) that is using it. This enabled each editor to implement this logic for interacting with language servers only once, and then it can be used for any language server!

This is great for us, language creators, because it means that once we implement a language server for our language, most of the work is done, and the work we need to do per each editor is manageable.

Right now WLS is used only by the VSCode Wasp language extension, but thanks to the nature of the Language Server Protocol, it should be relatively easy to add support for other editors too! Check this GH issue if you are interested in helping.

Setup

The best thing: there is nothing you, as a Wasp user, have to do to set up WLS! It already comes bundled with your installation of wasp → so if you can run wasp projects on your machine, you already have WLS, and it is always of the correct version needed for your current wasp installation. The only thing you need to ensure is you have wasp version ≥ 0.6, and a relatively fresh VSCode Wasp language extension.

An easy way to check that your version of wasp has WLS packaged into it is to run it and look at its usage instructions: it should mention waspls as one of the commands.

Wasp VSCode extension

If we would call Wasp Language Server (WLS) the “backend”, then VSCode Wasp language extension would be “frontend” → it takes care of everything to ensure you have a nice experience working with Wasp in VSCode, while delegating the hardest work to the WLS.

tip

For curious, you can check out its source code here, core of it is just one file: https://github.com/wasp-lang/vscode-wasp/blob/main/src/extension.ts

Features

Syntax highlighting

Nothing unexpected here: it recognizes different parts of Wasp syntax, like type, value, identifier, comment, string, … and colors them appropriately.

If you are curious how is this implemented, check https://github.com/wasp-lang/vscode-wasp/blob/main/syntaxes/wasp.tmLanguage.yaml → the whole syntax of Wasp is described via this “mysterious” old TextMate format, since that is the way to do it in VSCode.

Snippets

Wasp allows you to quickly generate a snippet of code for a new page, query, action, or entity!

Check out our snippet definitions here: https://github.com/wasp-lang/vscode-wasp/blob/main/snippets/wasp.json . It is actually really easy, in VSCode, to define them and add new ones.

Live error reporting + autocompletion

This is done by delegating the work to WLS, as described above!

IDE support for Javascript / Typescript in Wasp project

Due to how unique Wasp is in its approach, getting an IDE to provide all the usual features for Javascript / Typescript wasn’t completely working, and instead, the IDE would get somewhat confused with the context in which files are and would for example not be able to offer “go to definition” for some values, or would not know how to follow the import path.

With Wasp Beta this is now resolved! We resolved this by somewhat changing the structure of the Wasp project and also adding tsconfig.json files that provide IDE with the information needed to correctly analyze the JS/TS source files.

To learn more about Typescript support in Wasp Beta, check this blog post!

What does the future hold?

While Wasp Beta greatly improved IDE support for Wasp, there are still quite a few things we want to improve on:

  1. Smarter autocompletion via WLS.
    1. Right now it suggests any expression when you need an expression. In the future, we want it to know exactly what is the type of needed expression, and suggest only expressions of that type! So if I am in route ... { to: <my_cursor_here> }, then I want to see only pages among the suggested completions, not queries or actions or something else.
    2. Further, we would also like it to autocomplete on dictionary fields → so if I am in route ... { <my_cursor_here> }, it should offer me path and to as completions, as those are only valid fields in the route dictionary.
  2. Extensions for other editors besides VSCode. Now that we have Wasp Language Server, these shouldn’t be too hard to implement! This is also a great task for potential contributors: check this GH issue if you are interested.
  3. Implement Wasp code formatter. We could make it a part of WLS, and then have the editor extension call it on save.
  4. Improve support for PSL (Prisma Schema Language) in .wasp files.

If any of these sound interesting, feel free to join us on our Github, or join the discussion on Discord!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/12/08/fast-fullstack-chatgpt.html b/blog/2022/12/08/fast-fullstack-chatgpt.html index b962708403..7c4da435fe 100644 --- a/blog/2022/12/08/fast-fullstack-chatgpt.html +++ b/blog/2022/12/08/fast-fullstack-chatgpt.html @@ -19,13 +19,13 @@ - - + +
-

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

· 3 min read
Vinny

There’s a lot of hype around ChatGPT at the moment, and for good reason. It’s amazing. But there’s also some very valid criticism: that it’s simply taking the grunt work out of programming by writing boilerplate for us, which we as developers have to maintain!

I expected technology to make programming less laborious, as it does to most things. But I have to admit I expected it to happen by programmers switching to more powerful languages, rather than continuing to write programs full of boilerplate, but having AIs generate most of it.

PG is totally right in his remark above, but what he doesn’t realize is that there are languages out there that attempt to overcome this very problem, and Wasp is one of them.

What makes Wasp unique is that it’s a framework that uses a super simple language to help you build your web app: front-end, server, and deployment. But it’s not a complicated language like Java or Python, it’s more similar to SQL or JSON, so the learning curve is really quick (technically, it’s a Domain Specific Langauge or DSL).

Check it out for yourself:

main.wasp
app todoApp {
title: "ToDo App",/* visible in tab */

auth: {/* full-stack auth out-of-the-box */
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
}
}
}

route RootRoute { path: "/", to: MainPage }
page MainPage {
/* import your React code */
component: import Main from "@client/Main.js"
}

With this simple file above, Wasp will continually compile a truly full-stack web app for you, with a React front-end, and an ExpressJS server. You’re free to then build out the important features yourself with React, NodeJS, Prisma, and react-query.

The great part is, you can probably understand the Wasp syntax without even referencing the docs. Which means AI can probably work with it easily as well. So rather than having AI create a ton of boilerplate for us, we thought “can ChatGPT write Wasp?” If it can, all we need is to have it create that one file, and then the power of Wasp will take care of the rest. No more endless boilerplate!

So that’s exactly what we set to find out in the video above. The results? Well let’s just say they speak for themselves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

· 3 min read
Vinny

There’s a lot of hype around ChatGPT at the moment, and for good reason. It’s amazing. But there’s also some very valid criticism: that it’s simply taking the grunt work out of programming by writing boilerplate for us, which we as developers have to maintain!

I expected technology to make programming less laborious, as it does to most things. But I have to admit I expected it to happen by programmers switching to more powerful languages, rather than continuing to write programs full of boilerplate, but having AIs generate most of it.

PG is totally right in his remark above, but what he doesn’t realize is that there are languages out there that attempt to overcome this very problem, and Wasp is one of them.

What makes Wasp unique is that it’s a framework that uses a super simple language to help you build your web app: front-end, server, and deployment. But it’s not a complicated language like Java or Python, it’s more similar to SQL or JSON, so the learning curve is really quick (technically, it’s a Domain Specific Langauge or DSL).

Check it out for yourself:

main.wasp
app todoApp {
title: "ToDo App",/* visible in tab */

auth: {/* full-stack auth out-of-the-box */
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
}
}
}

route RootRoute { path: "/", to: MainPage }
page MainPage {
/* import your React code */
component: import Main from "@client/Main.js"
}

With this simple file above, Wasp will continually compile a truly full-stack web app for you, with a React front-end, and an ExpressJS server. You’re free to then build out the important features yourself with React, NodeJS, Prisma, and react-query.

The great part is, you can probably understand the Wasp syntax without even referencing the docs. Which means AI can probably work with it easily as well. So rather than having AI create a ton of boilerplate for us, we thought “can ChatGPT write Wasp?” If it can, all we need is to have it create that one file, and then the power of Wasp will take care of the rest. No more endless boilerplate!

So that’s exactly what we set to find out in the video above. The results? Well let’s just say they speak for themselves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/01/11/betathon-review.html b/blog/2023/01/11/betathon-review.html index 1d80574950..971d2b76cb 100644 --- a/blog/2023/01/11/betathon-review.html +++ b/blog/2023/01/11/betathon-review.html @@ -19,13 +19,13 @@ - - + +
-

Hosting Our First Hackathon: Results & Review

· 6 min read
Vinny

To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and that’s exactly what they did!

As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.

In this post, I’ll give you a quick run-down of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Tim’s Job Board

Tim's Job Board

Tim really went for it and created a feature-rich Job Board:

Wasp is very awesome! Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.” - Tim

🥈Chris’s “Cook Wherever” Recipes App

Chris's Cook Wherever Recipes App

Chris created an extensive database of recipes in a slick app:

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

🥉 Richard’s Roadmap & Feature Voting App

Richard’s Roadmap & Feature Voting App

I liked how Wasp simplified writing query/actions that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.” -

🥉 Emmanuel’s Notes App

Emmanuel’s Notes App

I joined the hackathon less than 48 hours before the submission deadline. Wasp made it look easy because it handled the hard parts for me. For example, username/password authentication took less than 7 lines of code to implement. - excerpt from Emmanuel’s Betathon Blog Post

Hackathon How-to

Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “launch week” approach when preparing for our own Beta launch and hacakthon.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & submission form

With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We weren’t certain we’d get many participants, so we decided to make the process as open as possible: two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our Betathon Homepage before the deadline. That was it.

When you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.

Keyboard
Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝

On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that we’d spotlight the winner’s on our platforms and social media accounts.

Promotion

For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a Beta Hacakthon homepage we created.

The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that they’re entering into a serious contest and committing to something of substance.

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response

The response overall was small but significant, considering Wasp’s age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something we’d do differently next time)!

We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so we’ll most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪


A big THANK YOU again to all the participants for their hard work and feedback. Here’s to the next one! 🍻

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hosting Our First Hackathon: Results & Review

· 6 min read
Vinny

To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and that’s exactly what they did!

As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.

In this post, I’ll give you a quick run-down of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Tim’s Job Board

Tim's Job Board

Tim really went for it and created a feature-rich Job Board:

Wasp is very awesome! Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.” - Tim

🥈Chris’s “Cook Wherever” Recipes App

Chris's Cook Wherever Recipes App

Chris created an extensive database of recipes in a slick app:

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

🥉 Richard’s Roadmap & Feature Voting App

Richard’s Roadmap & Feature Voting App

I liked how Wasp simplified writing query/actions that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.” -

🥉 Emmanuel’s Notes App

Emmanuel’s Notes App

I joined the hackathon less than 48 hours before the submission deadline. Wasp made it look easy because it handled the hard parts for me. For example, username/password authentication took less than 7 lines of code to implement. - excerpt from Emmanuel’s Betathon Blog Post

Hackathon How-to

Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “launch week” approach when preparing for our own Beta launch and hacakthon.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & submission form

With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We weren’t certain we’d get many participants, so we decided to make the process as open as possible: two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our Betathon Homepage before the deadline. That was it.

When you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.

Keyboard
Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝

On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that we’d spotlight the winner’s on our platforms and social media accounts.

Promotion

For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a Beta Hacakthon homepage we created.

The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that they’re entering into a serious contest and committing to something of substance.

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response

The response overall was small but significant, considering Wasp’s age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something we’d do differently next time)!

We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so we’ll most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪


A big THANK YOU again to all the participants for their hard work and feedback. Here’s to the next one! 🍻

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/01/18/wasp-beta-update-dec.html b/blog/2023/01/18/wasp-beta-update-dec.html index 681ec79f37..8a246fd61a 100644 --- a/blog/2023/01/18/wasp-beta-update-dec.html +++ b/blog/2023/01/18/wasp-beta-update-dec.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta December 2022

· 6 min read
Matija Sosic

Wasp Update Dec 22

Want to stay in the loop? → Join our newsletter!

Hey Wasp tribe 🐝 ,

Happy New Year! I know you're probably already sick of hearing it, but hopefully we're the last ones to congratulate you 🔫 👈 (that's pistol fingers emoji in case you were wondering).

Pistol fingers
This is how I imagine myself telling the joke above.

Now that the Beta Launch craze is over (thanks for your support, it was amazing - we saw more devs hacking with Wasp than ever!), we're back to our usual programming. Let's dive in and see what's new and what's in the plans for this year:

🎮 🐝 We hosted our first hackathon - it was a blast! 🎉 🎉

Tweet about Wasp

We launched our first Wasp hackathon ever on the last day of Beta Launch (thus we named it Betathon) and got some really cool submissions! Winners received hosting credits kindly offered by our partners at Railway and a special 1st place award was a wasp-themed mechanical keyboard (we're still assembling it but we'll post photos on our twitter :))!

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

To check out the winning projects and see where devs found Wasp most helpful, take a look here: Wasp Betathon review post

🔑 New auth method - GitHub! 🐙

Next to username/password and Google, Wasp now also supports GitHub as an authentication method!

Support for GitHub auth in Wasp

Putting the code above in your main.wasp file and specifying your GitHub env variables is all you need to do! Wasp will provide you with a full-stack GitHub authentication along with UI helpers (GitHub sign-up button) you can immediately use in your React component.

For more details, check the docs here.

💬 Let's discuss - on GitHub Discussions!

Wasp is now on GitHub Discussions

So far we've been capturing your feedback across GitHub issues and Wasp Discord server, but with the current volume it has become a bit unwieldy and hard to keep track of.

That's why we introduced Wasp GitHub Discussions! It's a relatively new service by GitHub that allows distinguishing between specific, well-defined issues (bug reports, TODOs, ...) and discussion items (ideating about new features, figuring out best practices, etc) and allows for upvotes from the community.

If there is a feature you'd like to see in Wasp (e.g. support for Vue) you can create a new post for it or upvote it if it is already there!

🚀 Next launch is coming - a super early sneak peek 👀

Next launch sneak peek

We know we just wrapped up Beta release, but we are busy wasps and our heads are already in the next one! We made a preliminary draft of the features that are going to be included - the "theme" of this release is going to be about making Wasp super easy and friendly for you to use.

We'll further polish our auth & deployment experience, along with ensuring TypeScript experience is fully typed and as helpful as possible. Stay tuned for the official roadmap and date of the next launch!

Want to make sure your fav feature makes it into the next release? Let us know on Discussions!

🎥 Wasp is now on YouTube!

Wasp is on YouTube

Thanks to Vince, who recently joined as Devrel (intro blog post coming soon!), Wasp now finally has its YouTube channel!

We're just starting out but already made some splashes - our "Build a full-stack app in 9 mins with Wasp and ChatGPT" got over 2k views (not bad for a channel with 50 subscribers, right?).

We also made our first YT short, featuring how to add auth to your app in 60 seconds with Wasp.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

🕹 Community highlights

Wasp Github Star Growth - over 2,000 ⭐️, woohoo!

Beta was great and it brought us to 2,234 stars! We never imagined Wasp could become so popular when we were just getting started. Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

And before you leave, here's a photo of a squishy wasp (ok, it's a bumblebee, but you get it) proudly rocking Wasp swag 🤘 🐝 (yep, we got a bunch of these for the office, you can also see Martin the background :D)!

Wasp's new mascot
This lil' boy actually became pretty popular in our community - we're now looking for a name for him!

Thanks for reading and see you in a month!

Buzzity buzz, you got that pizzazz 🐝 🐝,
+

Wasp Beta December 2022

· 6 min read
Matija Sosic

Wasp Update Dec 22

Want to stay in the loop? → Join our newsletter!

Hey Wasp tribe 🐝 ,

Happy New Year! I know you're probably already sick of hearing it, but hopefully we're the last ones to congratulate you 🔫 👈 (that's pistol fingers emoji in case you were wondering).

Pistol fingers
This is how I imagine myself telling the joke above.

Now that the Beta Launch craze is over (thanks for your support, it was amazing - we saw more devs hacking with Wasp than ever!), we're back to our usual programming. Let's dive in and see what's new and what's in the plans for this year:

🎮 🐝 We hosted our first hackathon - it was a blast! 🎉 🎉

Tweet about Wasp

We launched our first Wasp hackathon ever on the last day of Beta Launch (thus we named it Betathon) and got some really cool submissions! Winners received hosting credits kindly offered by our partners at Railway and a special 1st place award was a wasp-themed mechanical keyboard (we're still assembling it but we'll post photos on our twitter :))!

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

To check out the winning projects and see where devs found Wasp most helpful, take a look here: Wasp Betathon review post

🔑 New auth method - GitHub! 🐙

Next to username/password and Google, Wasp now also supports GitHub as an authentication method!

Support for GitHub auth in Wasp

Putting the code above in your main.wasp file and specifying your GitHub env variables is all you need to do! Wasp will provide you with a full-stack GitHub authentication along with UI helpers (GitHub sign-up button) you can immediately use in your React component.

For more details, check the docs here.

💬 Let's discuss - on GitHub Discussions!

Wasp is now on GitHub Discussions

So far we've been capturing your feedback across GitHub issues and Wasp Discord server, but with the current volume it has become a bit unwieldy and hard to keep track of.

That's why we introduced Wasp GitHub Discussions! It's a relatively new service by GitHub that allows distinguishing between specific, well-defined issues (bug reports, TODOs, ...) and discussion items (ideating about new features, figuring out best practices, etc) and allows for upvotes from the community.

If there is a feature you'd like to see in Wasp (e.g. support for Vue) you can create a new post for it or upvote it if it is already there!

🚀 Next launch is coming - a super early sneak peek 👀

Next launch sneak peek

We know we just wrapped up Beta release, but we are busy wasps and our heads are already in the next one! We made a preliminary draft of the features that are going to be included - the "theme" of this release is going to be about making Wasp super easy and friendly for you to use.

We'll further polish our auth & deployment experience, along with ensuring TypeScript experience is fully typed and as helpful as possible. Stay tuned for the official roadmap and date of the next launch!

Want to make sure your fav feature makes it into the next release? Let us know on Discussions!

🎥 Wasp is now on YouTube!

Wasp is on YouTube

Thanks to Vince, who recently joined as Devrel (intro blog post coming soon!), Wasp now finally has its YouTube channel!

We're just starting out but already made some splashes - our "Build a full-stack app in 9 mins with Wasp and ChatGPT" got over 2k views (not bad for a channel with 50 subscribers, right?).

We also made our first YT short, featuring how to add auth to your app in 60 seconds with Wasp.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

🕹 Community highlights

Wasp Github Star Growth - over 2,000 ⭐️, woohoo!

Beta was great and it brought us to 2,234 stars! We never imagined Wasp could become so popular when we were just getting started. Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

And before you leave, here's a photo of a squishy wasp (ok, it's a bumblebee, but you get it) proudly rocking Wasp swag 🤘 🐝 (yep, we got a bunch of these for the office, you can also see Martin the background :D)!

Wasp's new mascot
This lil' boy actually became pretty popular in our community - we're now looking for a name for him!

Thanks for reading and see you in a month!

Buzzity buzz, you got that pizzazz 🐝 🐝,
Matija, Martin and the Wasp team

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/01/31/wasp-beta-launch-review.html b/blog/2023/01/31/wasp-beta-launch-review.html index 7fb7ec7296..da2b45ae9b 100644 --- a/blog/2023/01/31/wasp-beta-launch-review.html +++ b/blog/2023/01/31/wasp-beta-launch-review.html @@ -19,13 +19,13 @@ - - + +
-

Convincing developers to try a new web framework - the effects of launching beta

· 7 min read
Matija Sosic

Alpha feedback

We are developing an OSS web framework in a form of a config language (DSL) that works with React & Node.js. Getting developers to use a new tool (especially a web framework) is a pretty hard thing to do. We wished there were more stories of how today's mainstream tools got adopted that we could learn from, so that motivated us to document our own.

Want to stay in the loop? → Join our newsletter!

TL;DR

  • HackerNews launch post brought the most traffic, by far
  • Product Hunt launch went worse than expected, bots took over
  • Our goal was to reach GitHub Trending but we failed
  • Less overall traffic than for the Alpha launch, but much higher quality of feedback + a shift in public perception
  • Having a public launch date made us 3x more productive

📊 The results: stats

We launched Beta on Nov 27, 2022 in a launch week format, recently popularized by Supabase. During the first week we launched on Product Hunt, and after the weekend we posted on HackerNews. Here's what the numbers were on the last day of the launch:

  • 190 GitHub stars added to the repo
  • 108 new projects started
  • 83 new users (installed Wasp locally and ran it)

Web visitors during beta launch week

HN launch caused almost 2x spike in traffic and usage. Also, although our launch week already ended by the start of December, we actually had the most users ever throughout December:

WAU displayed monthly

Looking back, this wasn't at all our biggest event in terms of traffic, but it was in terms of usage:

All time stats

One of the main effects of the launch (together with a few recent successful HN posts, and the Alpha Testing Program we ran in Jul '22) is that we managed to move the baseline WAU from ~10 to ~20. Another effect, felt more subjectively, is the change in the community perception.

Community perception shift

As mentioned above, although our Alpha launch had higher absolute numbers (website traffic, HN upvotes etc), it felt that Beta launch caused the biggest perception shift in the community so far.

Before were mostly getting superficial comments like “this looks cool, I’ll give it a try once”, or “why DSL approach and not the other one”, and this time we could notice that portion of people already knew Wasp from before (some even used it), and had more specific questions, even proposing next features that we planned but haven’t published yet.

Beta feedback

Although the core message (DSL for developing full-stack web apps with React & Node.js) hasn’t changed, there was significantly less pushback to the concept than before. I guess it comes down to the time elapsed and the product being more polished and validated from the outside - Beta, published use-cases, testimonials, …

Before the launch

This was our initial plan:

Launch timeline

For 20 days before the launch we were posting daily countdown banners on Twitter + a few polls (e.g. what's your favourite CSS framework) to engage the audience.

Examples of pre-launch tweets

Our Twitter game is still super young (~500 followers) so it didn't have a big effect but it helped to get the team excited and a few people also noticed it and commented/voted.

Due to the lack of time we ended up doing user testing in-house. That's still something I'd like to improve and make a habit of in the future.

A few other things we did prior to the launch:

  • Redesigned our project page - gave it a new, sleeker look
  • Published use cases with our most successful users and featured them on the project page
  • Activated our Discord and email list
  • Organized a launch event (call on Discord) to celebrate the launch - it went better than expected, a decent amount of people showed up and we had some good discussions!

The launch

As mentioned, we went with a launch week format - we liked the idea of having a whole week filled with content rather than cramming everything in a single day. We highlighted a new feature every day + launched a hackathon on the last day of the week, to keep the momentum. You can see the full schedule here.

Launch week schedule

We also shared our launch news at different places, most successful being Product Hunt, HackerNews and Reddit.

Product Hunt - failed, but ok

The mistake we did was launching on the Thanksgiving weekend - there was little (real) traffic + the mods were away so the bots took over!

We ended up as #5 product of the day with ~250 upvotes, which wasn’t so bad because in the end we got featured in their daily newsletter with 1M+ subscribers.

The bad part was that mods were away and pretty much all other products in front of us were fake or obviously bot powered! It felt like there was no real interaction on any of these products, just endless “congrats on the launch” comments from the newly created accounts with obviously fake names. Two products were also clearly violating PH rules (one was the same product that launched a week or two ago, but just changed the name).

The most disappointing part for us (and especially for the team) was that it felt like there aren’t any real people on PH, just bots.

🕹 Post-launch: Wasp Hackathon #1 - Betathon!

Since we introduced all the new features during the launch week, we thought a good way to keep the community engaged and give them a reason to try Wasp Beta out would be to throw a hackathon! It was the first time we did so we weren't sure how it'd go, but it went better than expected!

Tweet about Betathon - our #1 hackathon!

In the end, it was definitely worth it (see review and submissions here). It was quite lightweight to organize (we even made a custom web app with Wasp for the hackathon which you can also use for your hackathon) and we got some really nice submissions and community shout-outs.

Announcing a launch date publicly is great for productivity

Another big benefit we noticed from this type of launching is how much more productive it made the whole team. Although the launch date was totally self-imposed (and we did move it a couple of times internally), it was still an amazing forcing function once we announced it publicly. It focused the efforts of the whole team and it also felt great.

We decided to keep going with the quarterly release schedule in this format - 3 months is just enough time to make a dent on the product side, but not long enough to get stuck or caught up with endless refactoring. It also forces us to plan for the features that will have most impact on the developers using Wasp and make their lives easier, because we all want to have something cool and useful to present during the launch week.

Conclusion

I hope you found this post helpful or at least interesting! Creating a new web framework might be one of the most notorious things to do as a developer, but that shouldn't be a reason not to do it - where are the new frameworks going to come from otherwise?

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Convincing developers to try a new web framework - the effects of launching beta

· 7 min read
Matija Sosic

Alpha feedback

We are developing an OSS web framework in a form of a config language (DSL) that works with React & Node.js. Getting developers to use a new tool (especially a web framework) is a pretty hard thing to do. We wished there were more stories of how today's mainstream tools got adopted that we could learn from, so that motivated us to document our own.

Want to stay in the loop? → Join our newsletter!

TL;DR

  • HackerNews launch post brought the most traffic, by far
  • Product Hunt launch went worse than expected, bots took over
  • Our goal was to reach GitHub Trending but we failed
  • Less overall traffic than for the Alpha launch, but much higher quality of feedback + a shift in public perception
  • Having a public launch date made us 3x more productive

📊 The results: stats

We launched Beta on Nov 27, 2022 in a launch week format, recently popularized by Supabase. During the first week we launched on Product Hunt, and after the weekend we posted on HackerNews. Here's what the numbers were on the last day of the launch:

  • 190 GitHub stars added to the repo
  • 108 new projects started
  • 83 new users (installed Wasp locally and ran it)

Web visitors during beta launch week

HN launch caused almost 2x spike in traffic and usage. Also, although our launch week already ended by the start of December, we actually had the most users ever throughout December:

WAU displayed monthly

Looking back, this wasn't at all our biggest event in terms of traffic, but it was in terms of usage:

All time stats

One of the main effects of the launch (together with a few recent successful HN posts, and the Alpha Testing Program we ran in Jul '22) is that we managed to move the baseline WAU from ~10 to ~20. Another effect, felt more subjectively, is the change in the community perception.

Community perception shift

As mentioned above, although our Alpha launch had higher absolute numbers (website traffic, HN upvotes etc), it felt that Beta launch caused the biggest perception shift in the community so far.

Before were mostly getting superficial comments like “this looks cool, I’ll give it a try once”, or “why DSL approach and not the other one”, and this time we could notice that portion of people already knew Wasp from before (some even used it), and had more specific questions, even proposing next features that we planned but haven’t published yet.

Beta feedback

Although the core message (DSL for developing full-stack web apps with React & Node.js) hasn’t changed, there was significantly less pushback to the concept than before. I guess it comes down to the time elapsed and the product being more polished and validated from the outside - Beta, published use-cases, testimonials, …

Before the launch

This was our initial plan:

Launch timeline

For 20 days before the launch we were posting daily countdown banners on Twitter + a few polls (e.g. what's your favourite CSS framework) to engage the audience.

Examples of pre-launch tweets

Our Twitter game is still super young (~500 followers) so it didn't have a big effect but it helped to get the team excited and a few people also noticed it and commented/voted.

Due to the lack of time we ended up doing user testing in-house. That's still something I'd like to improve and make a habit of in the future.

A few other things we did prior to the launch:

  • Redesigned our project page - gave it a new, sleeker look
  • Published use cases with our most successful users and featured them on the project page
  • Activated our Discord and email list
  • Organized a launch event (call on Discord) to celebrate the launch - it went better than expected, a decent amount of people showed up and we had some good discussions!

The launch

As mentioned, we went with a launch week format - we liked the idea of having a whole week filled with content rather than cramming everything in a single day. We highlighted a new feature every day + launched a hackathon on the last day of the week, to keep the momentum. You can see the full schedule here.

Launch week schedule

We also shared our launch news at different places, most successful being Product Hunt, HackerNews and Reddit.

Product Hunt - failed, but ok

The mistake we did was launching on the Thanksgiving weekend - there was little (real) traffic + the mods were away so the bots took over!

We ended up as #5 product of the day with ~250 upvotes, which wasn’t so bad because in the end we got featured in their daily newsletter with 1M+ subscribers.

The bad part was that mods were away and pretty much all other products in front of us were fake or obviously bot powered! It felt like there was no real interaction on any of these products, just endless “congrats on the launch” comments from the newly created accounts with obviously fake names. Two products were also clearly violating PH rules (one was the same product that launched a week or two ago, but just changed the name).

The most disappointing part for us (and especially for the team) was that it felt like there aren’t any real people on PH, just bots.

🕹 Post-launch: Wasp Hackathon #1 - Betathon!

Since we introduced all the new features during the launch week, we thought a good way to keep the community engaged and give them a reason to try Wasp Beta out would be to throw a hackathon! It was the first time we did so we weren't sure how it'd go, but it went better than expected!

Tweet about Betathon - our #1 hackathon!

In the end, it was definitely worth it (see review and submissions here). It was quite lightweight to organize (we even made a custom web app with Wasp for the hackathon which you can also use for your hackathon) and we got some really nice submissions and community shout-outs.

Announcing a launch date publicly is great for productivity

Another big benefit we noticed from this type of launching is how much more productive it made the whole team. Although the launch date was totally self-imposed (and we did move it a couple of times internally), it was still an amazing forcing function once we announced it publicly. It focused the efforts of the whole team and it also felt great.

We decided to keep going with the quarterly release schedule in this format - 3 months is just enough time to make a dent on the product side, but not long enough to get stuck or caught up with endless refactoring. It also forces us to plan for the features that will have most impact on the developers using Wasp and make their lives easier, because we all want to have something cool and useful to present during the launch week.

Conclusion

I hope you found this post helpful or at least interesting! Creating a new web framework might be one of the most notorious things to do as a developer, but that shouldn't be a reason not to do it - where are the new frameworks going to come from otherwise?

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/02/02/no-best-framework.html b/blog/2023/02/02/no-best-framework.html index 1631b7162c..2f1df0989f 100644 --- a/blog/2023/02/02/no-best-framework.html +++ b/blog/2023/02/02/no-best-framework.html @@ -19,13 +19,13 @@ - - + +
-

The Best Web App Framework Doesn't Exist

· 3 min read
Vinny

The web app framework you choose doesn’t really matter. Well, it matters, just not as much as others would like you to believe.

The fact that so many libraries and frameworks exist in 2023, and that the best one is still hotly debated, proves my point. It’s the web developers biggest “first-world problem” — a problem that’s not really a problem. On Maslow’s Hierarchy of Developer Needs, it’s definitely near the top (ok, I made that up 😅)


hierarchy of developer needs


For example, according the the StateOfJS survey, there were 5 Front-end Frameworks with good retention in 2018, now there are 11 in 2022. That’s a 120% increase in a matter of 4 years, and that’s not even taking into account the hot meta-frameworks like NextJS, SvelteKit, or Astro!


State of JS 2022
A growing family of frameworks...


These are great developments for the space, overall. They improve things like developer speed, bundle size, performance, and developer experience. But they also make it damn hard for developers and teams to make a decision when trying to decide which to use for their next project. It’s even worse for beginners, which is probably why they just go for React — which, of course, is perfectly fine.

And I think all of this is OK, because in the end it doesn’t really matter which one you choose. When it really comes down to it, all that matters is that the framework you chose:

  • Is stable
  • Allows you to move quickly
  • Allows you to reach your end goal

Why? Because most of them are built around the same concepts, have proven themselves capable of performing at scale, and have communities you can engage with and learn from.

React might be the most prominent in job descriptions, but if you’re looking for a new role and only have experience in Vue or Angular, I can’t imagine it would take you more than a week to build a side-project with React to display your ability to prospective employers.

On the flip side, if you’re a beginner or Junior dev, once you have the basics of HTML, CSS, and JS under your belt, it doesn’t really matter what framework you learn. I personally started learning backend development with Node/ExpressJS, but landed my first role as a Frontend developer with Angular. In my second role I used NextJS, and now I work with Wasp (a full-stack framework built on top of React and ExpressJS). Developers never stop learning, so it’s kind of a non-argument to deride any specific framework — unless it really sucks, but then no one will continue to use it anyway.


Use what works


So, in the end, use what works. Because in 99.99% of cases, your choice of web framework will not decide the fate of your project.

If you’ve done a bit of research and found a framework that suits your needs and you enjoy using it — use it. There’s really no good reason not to.



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

The Best Web App Framework Doesn't Exist

· 3 min read
Vinny

The web app framework you choose doesn’t really matter. Well, it matters, just not as much as others would like you to believe.

The fact that so many libraries and frameworks exist in 2023, and that the best one is still hotly debated, proves my point. It’s the web developers biggest “first-world problem” — a problem that’s not really a problem. On Maslow’s Hierarchy of Developer Needs, it’s definitely near the top (ok, I made that up 😅)


hierarchy of developer needs


For example, according the the StateOfJS survey, there were 5 Front-end Frameworks with good retention in 2018, now there are 11 in 2022. That’s a 120% increase in a matter of 4 years, and that’s not even taking into account the hot meta-frameworks like NextJS, SvelteKit, or Astro!


State of JS 2022
A growing family of frameworks...


These are great developments for the space, overall. They improve things like developer speed, bundle size, performance, and developer experience. But they also make it damn hard for developers and teams to make a decision when trying to decide which to use for their next project. It’s even worse for beginners, which is probably why they just go for React — which, of course, is perfectly fine.

And I think all of this is OK, because in the end it doesn’t really matter which one you choose. When it really comes down to it, all that matters is that the framework you chose:

  • Is stable
  • Allows you to move quickly
  • Allows you to reach your end goal

Why? Because most of them are built around the same concepts, have proven themselves capable of performing at scale, and have communities you can engage with and learn from.

React might be the most prominent in job descriptions, but if you’re looking for a new role and only have experience in Vue or Angular, I can’t imagine it would take you more than a week to build a side-project with React to display your ability to prospective employers.

On the flip side, if you’re a beginner or Junior dev, once you have the basics of HTML, CSS, and JS under your belt, it doesn’t really matter what framework you learn. I personally started learning backend development with Node/ExpressJS, but landed my first role as a Frontend developer with Angular. In my second role I used NextJS, and now I work with Wasp (a full-stack framework built on top of React and ExpressJS). Developers never stop learning, so it’s kind of a non-argument to deride any specific framework — unless it really sucks, but then no one will continue to use it anyway.


Use what works


So, in the end, use what works. Because in 99.99% of cases, your choice of web framework will not decide the fate of your project.

If you’ve done a bit of research and found a framework that suits your needs and you enjoy using it — use it. There’s really no good reason not to.



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/02/14/amicus-indiehacker-interview.html b/blog/2023/02/14/amicus-indiehacker-interview.html index ab0ebbe7b7..dd0314177b 100644 --- a/blog/2023/02/14/amicus-indiehacker-interview.html +++ b/blog/2023/02/14/amicus-indiehacker-interview.html @@ -19,14 +19,14 @@ - - + +
-

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

· 8 min read
Vinny

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’ +

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

· 8 min read
Vinny

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’  Erlis Kllogjri


Erlis Kllogjri, a computer engineer and the creator of Amicus.work, went from idea to paying customers in just one week 🤯! In this interview, he tells how sometimes the best ideas come looking for you, and how moving quickly can help you stay inspired, motivated, and pull in your first satisfied customers.


Amicus Homepage


Before we begin with the unlikely origin story of Amicus.work, can you tell us a bit about what it is?

Amicus is a SaaS tool for legal teams that helps keep you organized and on top of your legal needs. Think of it like "Asana for lawyers", but with features and workflows tailored to the domain of law.

It allows attorneys and their clients to easily track the progress of the legal case they are dealing with, and collaborate with others involved in the case, all in one central location. For example, deadline reminders help with not missing key dates and workflow visualization allows lawyer and client to see where the process is stuck, and get it unstuck.

Your time from initial idea to working MVP seemed fast. How long was it and how did you achieve it so quickly?

From the initial discussions to the launch of the initial prototype was probably a week or so. This is even quicker than it sounds because I was working a full time job at the time. The speed [of execution] was fully enabled by Wasp, a full-stack web app framework.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

How were you able to get these first customers so quickly?

The first user is a little bit of a cheat because I know them — my brother, who is a lawyer. But having read about other entrepreneurs, this is not that uncommon. Sometimes the first users we know are ourselves, sometimes they’re family or friends, and sometimes it’s someone you sought out. But I think it was important to have the client before the idea, because that way you have the problem before the solution.

What advice would you give to other Solopreneurs regarding the validation process?

With regard to process, I spent a lot of time having discussions with my first user - my brother. The better you know the first user, the more careful you need to be I think. They’re going to give you slack and support your ideas. You don’t really want that, so you have to dive deeper into each problem/solution - like asking 5 why’s, so you can be more objective.

Once more users came on, I began sending out surveys about the key things I wanted to know. I also started setting up SQL queries and adding logs to answer questions about what kind of user was using what features the most etc. Being a solopreneur means you have to be even more careful about what you spend your time building.

MRR is low at the moment, around ~$90, and the first goal is to get to an MRR around ~$2,000. At that point I would be able to throw more time and resources at the application, increase the utility, and kick off a virtuous cycle of more revenue and utility.

That’s great. So rather than trying to find a clever idea, the idea found you.

It’s funny because I have all of these harebrained ideas that I’m always kicking around, thinking about how to validate them: MVPs, setting up a landing page that gets emails or deposits, etc.

Meanwhile my brother was telling me about this pain of managing matters that no tool really helped with. Clients want to know where the process is, how many steps are left, how they need to be reminded of important dates like contract deadlines, etc. So I agreed to build something to see if it would help. Wasp was instrumental here because if these steps had taken too long I would have probably lost interest and gotten distracted by something else. It allowed me to abstract all the details of a full stack app and focus on the product itself.

I built the prototype and it was TERRIBLE, it hurts to think back on that first version. But it was being used, and terrible though it was, it was still providing utility. And that was the point where it clicked the idea would work - if my first crude attempt was useful, and it would only get better with each iteration, there is a space here to provide so much value that some of it can be captured.

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’.

What’s been the biggest lessons learned as a result from building Amicus? If you could do it over, what would you do the same and what would you do differently?

I think one of the things I would do differently is spend a little more time at the beginning getting a full grasp on the use cases. I tried doing this with interviews with the first client. However once what was intended was built, I come across all of these questions that weren’t initially obvious. I have seen PMs in the past create paper mockups (or using Figma if there is time) and walking a person through what they would do - then all of a sudden these assumptions you both had bubble up. [I] would probably do something like that if possible.

What were your biggest concerns before getting started building Amicus? What problems did you know you wanted to avoid and how did you successfully achieve those goals?

[My] biggest concern when getting started building Amicus was honestly that it would go to the unfinished project graveyard. Once again, Wasp was key to resolving this. Being able to remove most of the redundancy involved in making a full stack app really helped me. It allowed me to focus on the interesting problems.

One of the things I have been trying to be careful to avoid is building things that aren’t needed or solving problems that don’t exist. It is very easy to get into the trap of thinking ‘oh this would be cool’ or ‘oh this extra thing might need to be build incase…’. I have been trying to be rigorous about validating features before building them (by talking to users or through the surveys), and unless theres a good reason to believe something is a problem I don’t spend my time fixing it. This is very hard, but it has allowed me to focus.


Wasp Logo

Have you done any form of advertising? press releases? How are you spreading the word about Amicus at the moment?

No advertising yet and no press releases either. Right now spreading of the word is mostly through word of mouth. Advertising can be a money pit, especially when you don’t know what you’re doing (and I probably don’t know what I am doing) so I want to first make sure I am at the point where users feel passionate enough about Amicus to where they tell others about it. Once I get there, advertising can have a bigger return even with my fumbling.

What made you decide to go it alone as a “Solopreneur”? Were you confident that you’d be able to tackle the challenge alone, and if so why?

This wasn’t so much a decision as something that came about one decision at a time. What initially started as just a handy app for my brother to use, naturally grew in scope and utility, and all of a sudden there was a business and I effectively became a solopreneur. Although I’ve always wanted to be an entrepreneur, I didn’t realize I had become a solopreneur until after the fact.


Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/02/21/junior-developer-misconceptions.html b/blog/2023/02/21/junior-developer-misconceptions.html index 01dd22d577..520f87619b 100644 --- a/blog/2023/02/21/junior-developer-misconceptions.html +++ b/blog/2023/02/21/junior-developer-misconceptions.html @@ -19,15 +19,15 @@ - - + +
-

The Most Common Misconceptions Amongst Junior Developers

· 6 min read
Vinny

High code quality only indirectly affects users. The main purpose is to keep development velocity high which benefits all stakeholders +

The Most Common Misconceptions Amongst Junior Developers

· 6 min read
Vinny

High code quality only indirectly affects users. The main purpose is to keep development velocity high which benefits all stakeholders  zoechi


We recently asked the web dev community on Reddit.com what the most common misconceptions are amongst junior developers, and we got a ton of great responses -- more than 270 to be exact.

Because there was so much to discuss, Matija and I decided to summarize the replies and give our own opinions in a longer-form YouTube video, which you can watch below.

You can also continue reading further for a summary of the main concepts.

The Most Common Themes

Among the responses were lots of great, specific examples, but we noticed a lot of common themes within them:

  • Code Quality
  • Managing Time & Expectations
  • Effective Communication & Teamwork

These seemed to be the topics senior devs had the most to say about. And it makes sense -- these are the things that, when you get to the core of the issues, can make or break almost any career.

It was also interesting to see that the top replies were issues that encompassed all of these themes. For example, take the top-voted reply:

Clean it up later
The most common misconception is that you're going to come back and clean that up later.

First Quality & Then Velocity

The top reply above touches on all three of the common themes we outlined, because within it is a message about quality -- about doing things correctly. And whenever you speak about quality, there is an inherent assumption that it takes longer, so we're also talking about time management. And, if you're a part of a team, you can't work effectively without good communication and teamwork.

Nevertheless, in the "quality" debate there were effectively two camps, with those who thought quality code was about:

  1. writing clean, readable code, that's easy to maintain
  2. writing code that gets shipped on time and works.

The balance between meeting deadlines, shipping features, and writing the best possible code is obviously a tricky one to get right. Some people had the opinion that business realities trump clean code patterns in the dash to meet deadlines and keep clients happy, while others thought that clean, quality code should be the priority, and that by making it a priority you can actually increase long-term velocity, even if short-term deadlines aren't met.

You don't have to touch all the code you see

This discussion can distract from Junior developers priorities though, which are to grow and improve as a developer, not lead the team to success. Therefore, it's probably best for Junior devs to focus on quality first, and then improve their speed of delivery second.

Stay Humble & Manage Expectations

As a Junior developer, it's not expected that you're going to get everything right the first time. There is an assumption that you will learn the best practices over time, and along the way you might produce inconsistent work, make mistakes, or even possibly break some things along the way.

But that's okay.

It's part of the process. It's expected. And it's important to remember that this is not a reflection of your value or worth as an engineer or individual.

In the replies, there were also many developers who recognized another developer's desire "to fix things later" as a way to brush off criticism towards their work. They generally viewed this as a bad habit to get into, as it is often one that plagues developers even as they gain more experience. "Code reviews are not personal", and being able to take criticism graciously is an important skill to develop. After all, seniors are there to guide you towards making better decisions based on their own experiences. And juniors are there to learn.

The senior dev doesn't know everything

But how often should you seek a Senior's advice? Should you do what they said, or what some dude told you is the only way to do x on YouTube or in some blogpost ;) ?

Should you ask for help every time you get stuck, or should you compromise your sanity and struggle alone for days?

Well, it depends on who you ask. But most of the replies made it clear that:

  1. You should try it out yourself first.
  2. Use the resources available to you (Google, Stack Overflow, GPT) to try and figure it out.
  3. Ask for help once you considerably slow down on making any progress.
  4. If you have a possible solution and it differs from the senior dev's suggestion, that doesn't mean it's wrong -- there can sometimes be many possible ways to achieve the same goal!

Bothering seniors with questions

Be Flexibile & Open to Change

Nothing changes faster than the world of technology. As a developer, you need to constantly be learning and adapting to new technologies and trends. If you don't like change, well then being a software developer probably isn't the right career for you.

Everything takes longer than you think

On top of things changing constantly, it's the kind of job that challenges your assumptions. What you think might be the best solution turns out to be incompatible with your team's desired goals or end product, and you're forced to use a "sub-optimal" solution instead. Why? Because it's the best way to get the job done given your team's constraints. "Sorry, pal, but we can't use your favorite framework on this one."

The developers who stay flexible and open-minded are often at an advantage here. They're the ones that are less dogmatic about a particular technology or approach, and are more willing to adapt to the situation at hand. They're typically the ones that progress faster than their peers, and they're the ones that get the job done well.


Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/03/02/wasp-beta-update-feb.html b/blog/2023/03/02/wasp-beta-update-feb.html index bf8192f587..725c900198 100644 --- a/blog/2023/03/02/wasp-beta-update-feb.html +++ b/blog/2023/03/02/wasp-beta-update-feb.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta - February 2023

· 6 min read
Matija Sosic

Wasp Update Feb 23

Want to stay in the loop? → Join our newsletter!

Hey Wasp acolytes (Waspolytes?) 🐝,

What's kickin'? We at Wasp spent the whole month thinking of the coolest features to add to our next release and we can't wait to share it with you!

Tell me now
Ok ok, we're getting there, chill!

Let me cut to the chase and show you what's been cooking in Wasp pot for the past month:

Deploy to Fly.io with a single command for free 🚀☁️

Deploy to fly.io with single command

This is the only command you need to run to deploy your full app (client, server, and database) to Fly.io! They also offer a generous free tier so you can deploy your v1 without any second thoughts.

Check out our docs for more details: Deploying your Wasp app to Fly.io

✅ Full stack TypeScript support

Types everywhere

This is one of the features we are most excited about! Now, when you define an entity in your Wasp file, it immediately becomes accessible as a type both on a client and a server.

Full stack TypeScript support

This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started!

For more details, check out our entity docs.

🗓 We set a date for the next launch - April 11th! 🚀

Launch party

Mark your calendars, it's official! We will release the next version of Wasp on April 11th - in exactly 40 days! As the last time, we will follow a launch week format with a lot of memes, swag and fun prizes (Including Da Boi, of course).

Here's a quick list of the planned features:

  • Using Vite instead of CRA under the hood - you'll be able to create new Wasp apps in a blink of an eye! 🚀
  • Custom API routes
  • Code scaffolding for the quicker start
  • Support for sending emails
  • Password reset via email
  • Improved Auth UI
  • Testing support

And more! This is quite an ambitious plan but we are fully committed to getting it done. Any comments or ideas, ping us on our Discord.

☎️ We had our Community Call #2 - meet Da Boi

We had a community call

We had so much fun on our last community call that we decided we have to do it again! As you can notice, our community-approved mascot Da Boi stole the show. The rest was pretty much just a filler and an excuse to have more fun with Da Boi :D.

On a serious note, it was great to catch up with the community prior to the next release - we discussed features and the roadmap and everybody shared what they're building and what they'd like to see next in Wasp.

🎥 Wasp is now on YouTube!

Wasp is on YouTube

We are still going strong with our YouTube! The latest video started as a question on Reddit and it escalated quite quickly, with 200+ comments - we cover the responses we received + our expert commentary :D.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

⌨️ From the blog

🕹 Community highlights

  • PhraseTutor: Learn Italian in a week! There is a new app built from scratch with Wasp, by Mihovil - one of our early community members who recently joined the team as an engineer! It's smooth both on the front end and back end and will teach you Italian before you can say (or eat) "quattro formaggi"!

    Phrase Tutor

Developer life 💻⌨️💽

Here is the cool stuff we came across this month

Wasp Github Star Growth - 2,317 ⭐️, woohoo!

Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! Thanks for reading and we can't wait for our next launch to get out and see how you like it. As always, we're on Discord and appreciate any comments, feedback, and ideas - that's how Wasp came to be!

As a parting gift, here are a few curated Da Boi memes created by our valued community members:

Wasp's new mascot

Buzzy buzz, you got that snazz 🐝 🐝,
+

Wasp Beta - February 2023

· 6 min read
Matija Sosic

Wasp Update Feb 23

Want to stay in the loop? → Join our newsletter!

Hey Wasp acolytes (Waspolytes?) 🐝,

What's kickin'? We at Wasp spent the whole month thinking of the coolest features to add to our next release and we can't wait to share it with you!

Tell me now
Ok ok, we're getting there, chill!

Let me cut to the chase and show you what's been cooking in Wasp pot for the past month:

Deploy to Fly.io with a single command for free 🚀☁️

Deploy to fly.io with single command

This is the only command you need to run to deploy your full app (client, server, and database) to Fly.io! They also offer a generous free tier so you can deploy your v1 without any second thoughts.

Check out our docs for more details: Deploying your Wasp app to Fly.io

✅ Full stack TypeScript support

Types everywhere

This is one of the features we are most excited about! Now, when you define an entity in your Wasp file, it immediately becomes accessible as a type both on a client and a server.

Full stack TypeScript support

This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started!

For more details, check out our entity docs.

🗓 We set a date for the next launch - April 11th! 🚀

Launch party

Mark your calendars, it's official! We will release the next version of Wasp on April 11th - in exactly 40 days! As the last time, we will follow a launch week format with a lot of memes, swag and fun prizes (Including Da Boi, of course).

Here's a quick list of the planned features:

  • Using Vite instead of CRA under the hood - you'll be able to create new Wasp apps in a blink of an eye! 🚀
  • Custom API routes
  • Code scaffolding for the quicker start
  • Support for sending emails
  • Password reset via email
  • Improved Auth UI
  • Testing support

And more! This is quite an ambitious plan but we are fully committed to getting it done. Any comments or ideas, ping us on our Discord.

☎️ We had our Community Call #2 - meet Da Boi

We had a community call

We had so much fun on our last community call that we decided we have to do it again! As you can notice, our community-approved mascot Da Boi stole the show. The rest was pretty much just a filler and an excuse to have more fun with Da Boi :D.

On a serious note, it was great to catch up with the community prior to the next release - we discussed features and the roadmap and everybody shared what they're building and what they'd like to see next in Wasp.

🎥 Wasp is now on YouTube!

Wasp is on YouTube

We are still going strong with our YouTube! The latest video started as a question on Reddit and it escalated quite quickly, with 200+ comments - we cover the responses we received + our expert commentary :D.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

⌨️ From the blog

🕹 Community highlights

  • PhraseTutor: Learn Italian in a week! There is a new app built from scratch with Wasp, by Mihovil - one of our early community members who recently joined the team as an engineer! It's smooth both on the front end and back end and will teach you Italian before you can say (or eat) "quattro formaggi"!

    Phrase Tutor

Developer life 💻⌨️💽

Here is the cool stuff we came across this month

Wasp Github Star Growth - 2,317 ⭐️, woohoo!

Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! Thanks for reading and we can't wait for our next launch to get out and see how you like it. As always, we're on Discord and appreciate any comments, feedback, and ideas - that's how Wasp came to be!

As a parting gift, here are a few curated Da Boi memes created by our valued community members:

Wasp's new mascot

Buzzy buzz, you got that snazz 🐝 🐝,
Matija, Martin and the Wasp team

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html b/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html index aa9a6f4957..379c27a21f 100644 --- a/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html +++ b/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html @@ -19,13 +19,13 @@ - - + +
-

10 "Hard Truths" All Junior Developers Need to Hear

· 4 min read
Vinny

hard truths for junior devs

Ok, I have to admit, these aren’t really Truths, but rather some opinions I’ve formed over my journey switching careers from Educator to Developer.

It’s well known at this point that software — especially web — development is a viable option for someone looking for a new career without going the traditional education route. Due to this, and the fact that salaries tend to be very good, I think a portion of people making the switch might be doing it for the wrong reasons.

And once you get into that career, as a Junior it can often be difficult to know what you should be doing to advance your career. There are a ton of opinions out there (including mine) and juniors tend to develop a lot of misconceptions, as my colleague and I discussed in our recent Reddit post and follow-up video.

So, I put together this list of things you should consider when starting out a career in tech:

  1. 👎 If you’re doing it solely for the money, you’re not gonna make it. True, you don’t need a degree or anyone’s permission to advance in this career, but you need ambition and mental stamina. A genuine interest is needed to maintain them.

  2. 😎 You don’t have to follow the trends. Follow what interests you. Like I said before, you need mental stamina in this field of work. Following your interests will keep you engaged and help avoid burnout.

  3. 👩‍💻 You don’t need to know a piece of tech inside and out, contrary to what some devs might want you to believe. The truth is, you are always learning, and there will always be gaps in your knowledge. Your confidence in being able to fill those gaps is what matters.

  4. 🧱 Start building, ASAP. Find a problem that interests you and build the solution yourself. Contribute to Open-Source projects that you use. A portfolio of unique work speaks volumes about your abilities. Plus, there’s no better teacher than experience.

  5. 😱 Be fearless and seek feedback. Put your work out there and be ready to have it criticized. If you can stomach it, you’ll come out the other side a much better developer.

  6. 🧐 You should have a firm understanding of what you’re doing. Don’t copy-paste someone else’s answer (or GPT’s) to your problem and call it a day. Question why things work, and figure it out for yourself.

  7. 🏋️‍♀️ You have to do the grunt work, unfortunately. Don’t expect high salaries from the beginning. And you’ll probably want to improve your portfolio by working on side projects in your free time, or you might stay a junior dev for longer than you wish.

  8. 🧗‍♂️ Challenge yourself. Don’t let yourself get too comfortable. If you do, you won’t improve. Offer to take new, difficult, and daunting tasks at work or with your personal projects. You’ll be surprised what you can achieve.

  9. 💰 You don’t have to pay for boot camps or courses. In fact, you’re better off tackling problems on your own and only asking for help if you’re truly stuck. There’s a wealth of free resources out there, and when you’re on the job, these might be the only things to assist you.

  10. 🗣 Programming is definitely not the only skill you’ll need. Being respectful, communicative, conscientious, ambitious, and humble will put you in a different league and make you a valuable asset in any tech team.

TIP: Looking for some inspiration? Feedback? Motivation? Join us over at the Wasp Discord server, where we've got an active, friendly community of web developers of all skill levels that build side-projects, share their experiences, make memes, and chat about life



Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

10 "Hard Truths" All Junior Developers Need to Hear

· 4 min read
Vinny

hard truths for junior devs

Ok, I have to admit, these aren’t really Truths, but rather some opinions I’ve formed over my journey switching careers from Educator to Developer.

It’s well known at this point that software — especially web — development is a viable option for someone looking for a new career without going the traditional education route. Due to this, and the fact that salaries tend to be very good, I think a portion of people making the switch might be doing it for the wrong reasons.

And once you get into that career, as a Junior it can often be difficult to know what you should be doing to advance your career. There are a ton of opinions out there (including mine) and juniors tend to develop a lot of misconceptions, as my colleague and I discussed in our recent Reddit post and follow-up video.

So, I put together this list of things you should consider when starting out a career in tech:

  1. 👎 If you’re doing it solely for the money, you’re not gonna make it. True, you don’t need a degree or anyone’s permission to advance in this career, but you need ambition and mental stamina. A genuine interest is needed to maintain them.

  2. 😎 You don’t have to follow the trends. Follow what interests you. Like I said before, you need mental stamina in this field of work. Following your interests will keep you engaged and help avoid burnout.

  3. 👩‍💻 You don’t need to know a piece of tech inside and out, contrary to what some devs might want you to believe. The truth is, you are always learning, and there will always be gaps in your knowledge. Your confidence in being able to fill those gaps is what matters.

  4. 🧱 Start building, ASAP. Find a problem that interests you and build the solution yourself. Contribute to Open-Source projects that you use. A portfolio of unique work speaks volumes about your abilities. Plus, there’s no better teacher than experience.

  5. 😱 Be fearless and seek feedback. Put your work out there and be ready to have it criticized. If you can stomach it, you’ll come out the other side a much better developer.

  6. 🧐 You should have a firm understanding of what you’re doing. Don’t copy-paste someone else’s answer (or GPT’s) to your problem and call it a day. Question why things work, and figure it out for yourself.

  7. 🏋️‍♀️ You have to do the grunt work, unfortunately. Don’t expect high salaries from the beginning. And you’ll probably want to improve your portfolio by working on side projects in your free time, or you might stay a junior dev for longer than you wish.

  8. 🧗‍♂️ Challenge yourself. Don’t let yourself get too comfortable. If you do, you won’t improve. Offer to take new, difficult, and daunting tasks at work or with your personal projects. You’ll be surprised what you can achieve.

  9. 💰 You don’t have to pay for boot camps or courses. In fact, you’re better off tackling problems on your own and only asking for help if you’re truly stuck. There’s a wealth of free resources out there, and when you’re on the job, these might be the only things to assist you.

  10. 🗣 Programming is definitely not the only skill you’ll need. Being respectful, communicative, conscientious, ambitious, and humble will put you in a different league and make you a valuable asset in any tech team.

TIP: Looking for some inspiration? Feedback? Motivation? Join us over at the Wasp Discord server, where we've got an active, friendly community of web developers of all skill levels that build side-projects, share their experiences, make memes, and chat about life



Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html b/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html index 1f9c299e84..a072c6e108 100644 --- a/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html +++ b/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html @@ -19,13 +19,13 @@ - - + +
-

Building a full-stack app for learning Italian: Supabase vs. Wasp

· 14 min read
Mihovil Ilakovac

wasp vs. supabase

Intro

What to expect

In this blog post, I will explain how I created the Phrase Tutor app for learning Italian phrases using two different technologies. I will share some code snippets to show what was required to build the app with both Wasp and Supabase.

Phrase Tutor’s front-end
Phrase Tutor’s front-end

As a senior full-stack developer with experience in building many side-projects, I prefer a quick development cycle. I enjoy turning ideas into POCs in just a few days or even hours.

We will examine how each technology can help when building a full-stack app and where Wasp and Supabase excel.

I wanted to learn Italian fast

Whenever I travel abroad, I enjoy imagining what it would be like to live in that place. For instance, I usually don't like taking crowded public transportation, but for some reason, it brings me joy when I do it in a foreign country. It's all about the feeling that I'm living there. One of the most important things for me to fully experience the culture is to learn the language or, at the very least, be able to not speak English all the time.

Pretending to be Italian
Pretending to be Italian

My girlfriend and I were planning a trip to Italy, and I wanted to learn some Italian. I thought about what would be the easiest way to learn as much as possible with the least amount of effort. I decided that learning the top 100 Italian phrases would be a good start. I had a week to do it, and learning 100 phrases seemed doable if I practiced every day.

The learning method

In high school, I had a system for learning historical facts and dates quickly called "focusing on things you don’t know".

Here's how it works:

  1. Gather a pool of facts you want to learn (e.g. "When did WWI start?" - "1914").
  2. Ask yourself each question in the pool.
  3. If you know the answer, remove the fact from the pool.
  4. If you don't know the answer, keep it in the pool.
  5. Repeat with the smaller pool until there are no more facts left.

I made a small app for this and shared it with my classmates, but it didn't go further than that.

Now, I want to use the same method to learn Italian phrases for my trip. So, as a better developer now, I'll make a proper app and host it somewhere 🙂

Building the Phrase Tutor app

We will create an app that follows the method described above. The app will show you a phrase and you can tell it if you know the translation or not by selecting "I knew it" or "I didn't know it".

How the learning in the app should work
How the learning in the app should work

The app will keep track of your answers and suggest which phrases you should learn next 🕵️

I’ve built the app twice: first with Supabase and then with Wasp. Supabase is a well-rounded open-source Backend as a Service (BaaS) product that adds superpowers to your front-end apps. On the other hand, Wasp is an open-source framework for building full-stack apps that helps to keep the boilerplate low. Let’s see how they compare.

Initial Supabase version

When I made the initial version, I worked heavily with Vue.js, which I used to create the first version of the Phrase Tutor app. I started by collecting some phrases. I searched on Google for "best Italian phrases to learn" and came across an article titled "100 Italian phrases to learn." (After extracting the phrases from the HTML, I found out that there were only 96 phrases, but that was still good enough for me.)

The initial app contained the phrases in a JSON file that the frontend loaded. It was completely static, but it worked.

{
"id": 1,
"group": "general",
"translations": {
"en": "Yes",
"it": "Si"
}
}

I put it on Cloudflare Pages and it went live.

I showed it to my girlfriend, but she didn't like some of the phrases I used. If only I had a backend with a database to edit the phrases. Then I had an idea: let's add a database with Supabase.

Supabase is a managed backend solution that provides a lot of free stuff: a PostgreSQL database and social authentication among other things.

Phrase Tutor built with Supabase
Phrase Tutor built with Supabase

I set up the database tables using the Supabase UI which was pretty straightforward.

The table I needed only had a few fields:

CREATE TABLE phrases (
id bigint NOT NULL,
group character varying NULL,
translations_en text NOT NULL,
translations_it text NOT NULL
);

Then I had to seed the database with some SQL. Executing SQL statements is easy with the use of Supabase’s UI. You just log in, open the SQL editor and paste in the code:

INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (1,'general','Yes','Si');
INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (2,'general','No','No');
...

Integrating Supabase into my existing front-end app was simple using their Javascript SDK. If you're familiar with Firebase, it should feel similar. Essentially, you build your SQL queries on the frontend and use the resulting data in your app.

Using the SDK felt pretty straightforward and I could get what I wanted out of the database without much hassle.

const { data, error } = await supabase.from("phrases").select("*");

And just like that, my static Vue.js app had a database to rely on 🎉

Adding the login with Google was a matter of enabling it in Supabase UI and setting up the Client ID and Client Secret variables. In order to trigger the login process with Google, I once again relied on their Javascript SDK.

supabase.auth.signInWithOAuth({ provider: "google" });

Awesome! I'm glad that I can now edit the phrases and that there is a login feature that I plan to use later.

In the future, I have plans to add more languages to the app and also allow registered users to contribute new phrases and translations. I believe this will make the app more useful and engaging for language learners.

And just like that, my app went from a pure static app to an app with a database and Google login 🤯

info

Check out the deployed app written with Vue.js and Supabase: https://phrase-tutor.pages.dev

info

View the source here

Joining Wasp and dogfooding it

Some background before the second part: I started working at Wasp earlier this year. I'm really happy to work on a technology that solves a problem I care about: when I do side-projects, I dislike writing the same dull parts every time from scratch. I copy and paste from my previous side projects, but eventually, the code snippets become old and outdated.

Naturally, I wanted to test out Wasp by rewriting one of my side projects. I decided to see how Wasp could work with the Phrase Tutor project.

Wasp works by having an easy-to-understand config file called main.wasp which coordinates your pieces of client and server functionalities. Its main purpose is to keep you productive and focused on writing interesting bits. It feels pretty much like using a web framework that covers your whole app.

Phrase Tutor built with Wasp
Phrase Tutor built with Wasp

Let's begin by creating the data models. Wasp uses Prisma under the hood to communicate with your database, which makes it easy to manage your database without worrying about the details. This is just one of the many choices the framework made for me, and I appreciate the feeling of using a setup that works.

I had to first declare all of the entities I needed with Prisma PSL in the Wasp config file.

entity Phrase {=psl
id Int @id @default(autoincrement())
group String
phrase String
translations Translation[]
psl=}

entity Language {=psl
id Int @id @default(autoincrement())
name String @unique
emoji String
translations Translation[]
psl=}

entity Translation {=psl
id Int @id @default(autoincrement())
phraseId Int
languageId Int
translation String
phrase Phrase @relation(fields: [phraseId], references: [id], onDelete: Cascade)
language Language @relation(fields: [languageId], references: [id], onDelete: Cascade)
psl=}

I'm using a PostgreSQL database again, and you can see that the field definitions are similar.

I improved the data schema a bit by defining three tables instead of one. I separated the concept of a Phrase from the concepts of Language and Translation. This will make it easier to add new languages in the future.

I added some phrases to the database using Prisma and a Wasp action:

export async function seedItalianPhrases(args, context) {
const data = [
{
id: 1,
group: "general",
translations_en: "Yes",
translations_it: "Si"
},
...
]
for (const phrase of seedPhrases) {
await context.entities.Phrase.create({
...
});
}
}

Let’s now look at what I needed to do to get the data flowing from the backend to my React app.

First, I declared a query in my Wasp config file:

app phraseTutor {
...
}
...

query fetchAllPhrases {
fn: import { getAllPhrases } from "@server/queries.js",
entities: [Phrase]
}

Then I wrote the code for my backend to fetch the phrases. You’ll notice it’s quite similar to the code I wrote for fetching phrases with the Supabase SDK, but I had to include the translations relation since we now have multiple tables.

// My query got the Prisma entity through the context parameter
// which I just used to fetch all the phrases
export async function getAllPhrases(args, context) {
return context.entities.Phrase.findMany({
include: {
translations: true
}
});
}

And lastly, I could just import the query into my React app. It’s set up in a way that it handles cache invalidation automatically, one less thing to worry about, which is awesome 😎

// Wasp relies on React Query in the background
const { data: phrases, isLoading } = useQuery(fetchAllPhrases);

Let’s also add support for Google auth for our app. It involves declaring you want it in the Wasp file, adding some env variables and using it in the React application.

We declare it to the Wasp file by adding the google key under auth:

app phraseTutor {
...
auth: {
userEntity: User,
externalAuthEntity: SocialUser,
methods: {
// Define we want the Google auth
google: {
// Optionally, we can adjust what is saved from the user's data
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/"
},
...
}

// Some of the entities needed for auth
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
profilePicture String
externalAuthAssociations SocialUser[]
createdAt DateTime @default(now())
psl=}

entity SocialUser {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

And … that’s it. We can now use the Google auth in our frontend 🎉

import { signInUrl as googleSignInUrl } from "@wasp/auth/helpers/Google";
...
const { data: user } = useAuth();

Writing a full-stack React and Express.js with Wasp felt like a guided experience; I didn't have to focus too hard on the dev tooling, building, or deploying.

Instead, I could focus on the logic needed for Phrase Tutor to work and just run wasp start most of the time. I did need to write some extra code to get everything running, but I'm free to customize this code however I want.

info

Check out the deployed project built with Wasp: https://phrasetutor.com

info

View the source here

Let's compare some of the features

I want to compare the features of Supabase and Wasp. It's good to think about different ways to do things and their pros and cons.

FeatureSupabaseWasp
Getting data from the APIUse the Supabase JS SDK to query database tablesDeclare query in Wasp config and use Prisma JS SDK to implement it
Custom business logicWriting custom PostgreSQL procedures or by writing edge functionsDeclare actions in the Wasp file and write server-side JS
Defining the database schemaVisual editor or by CREATE TABLE queryBy code - edit Prisma schema and commit changes
AuthEnable in UIEnable it in the Wasp file
DeploymentSupabase managed instance or self-host itDeploy anywhere, support for https://fly.io one line deployment

With Supabase, I liked how familiar the SDK felt and their UI made it easy to configure parts of my backend. I didn’t need to think about deploying Supabase since I used their hosted version, but it did get paused after 1 week of inactivity on the free tier.

On the other hand, Wasp felt like the glue for my React + Express.js + Prisma app and I needed to write more code to get things done. It felt more explicit because I wrote code closer to what I would normally write. I deployed it to fly.io with the Wasp command wasp deploy fly launch and it’s now live on https://phrasetutor.com

Conclusion

It's all about the use case

Choosing the right solution for your needs can be difficult. That's why it's important to try out different options and see how they work for you. In this case, I compared two options: Supabase and Wasp.

Supabase is a great choice if you want a well-rounded open-source BaaS product that adds superpowers to your front-end apps. It provides a lot of free stuff, such as a PostgreSQL database and social authentication, which can make development easier and faster. It also has a nice SDK and UI that the end user can use to easily define their app's configuration.

Wasp is an open-source framework for building full-stack apps that helps out with keeping the boilerplate low. It is a bit more explicit about some things, such as defining your auth entities, but that can be a plus when you have more advanced use cases. By using Wasp as the glue for your full-stack application, you can have the best of both worlds: a development and production setup that works out of the box while still allowing you to develop your app any way you like.

In the case of Phrase Tutor, I liked working with both Supabase and Wasp. I did, however, get a different feeling from working with the two technologies. With Supabase I felt like my front-end app got instant superpowers and it now has a database and login, which was nice considering the effort I had to put in. But now I had a black-box dependency that I needed to build around.

When I used Wasp to rebuild Phrase Tutor, it felt different because it was a full-stack app. I had more control over the application code, so I could change it and evolve it as I wanted. I felt like I had built an app that could grow in any direction. Although I had to write more code, it felt like a good trade-off for future needs.

To decide which option is best for you, I would suggest trying both and seeing how you feel. It is easy to set up both tools and see if they make sense for you.

Grazie for reading 🙃
Grazie for reading 🙃

If you try out the Phrase Tutor app, please let me know what you think. You can reach me on Twitter. I'm always looking for ways to make it better.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Building a full-stack app for learning Italian: Supabase vs. Wasp

· 14 min read
Mihovil Ilakovac

wasp vs. supabase

Intro

What to expect

In this blog post, I will explain how I created the Phrase Tutor app for learning Italian phrases using two different technologies. I will share some code snippets to show what was required to build the app with both Wasp and Supabase.

Phrase Tutor’s front-end
Phrase Tutor’s front-end

As a senior full-stack developer with experience in building many side-projects, I prefer a quick development cycle. I enjoy turning ideas into POCs in just a few days or even hours.

We will examine how each technology can help when building a full-stack app and where Wasp and Supabase excel.

I wanted to learn Italian fast

Whenever I travel abroad, I enjoy imagining what it would be like to live in that place. For instance, I usually don't like taking crowded public transportation, but for some reason, it brings me joy when I do it in a foreign country. It's all about the feeling that I'm living there. One of the most important things for me to fully experience the culture is to learn the language or, at the very least, be able to not speak English all the time.

Pretending to be Italian
Pretending to be Italian

My girlfriend and I were planning a trip to Italy, and I wanted to learn some Italian. I thought about what would be the easiest way to learn as much as possible with the least amount of effort. I decided that learning the top 100 Italian phrases would be a good start. I had a week to do it, and learning 100 phrases seemed doable if I practiced every day.

The learning method

In high school, I had a system for learning historical facts and dates quickly called "focusing on things you don’t know".

Here's how it works:

  1. Gather a pool of facts you want to learn (e.g. "When did WWI start?" - "1914").
  2. Ask yourself each question in the pool.
  3. If you know the answer, remove the fact from the pool.
  4. If you don't know the answer, keep it in the pool.
  5. Repeat with the smaller pool until there are no more facts left.

I made a small app for this and shared it with my classmates, but it didn't go further than that.

Now, I want to use the same method to learn Italian phrases for my trip. So, as a better developer now, I'll make a proper app and host it somewhere 🙂

Building the Phrase Tutor app

We will create an app that follows the method described above. The app will show you a phrase and you can tell it if you know the translation or not by selecting "I knew it" or "I didn't know it".

How the learning in the app should work
How the learning in the app should work

The app will keep track of your answers and suggest which phrases you should learn next 🕵️

I’ve built the app twice: first with Supabase and then with Wasp. Supabase is a well-rounded open-source Backend as a Service (BaaS) product that adds superpowers to your front-end apps. On the other hand, Wasp is an open-source framework for building full-stack apps that helps to keep the boilerplate low. Let’s see how they compare.

Initial Supabase version

When I made the initial version, I worked heavily with Vue.js, which I used to create the first version of the Phrase Tutor app. I started by collecting some phrases. I searched on Google for "best Italian phrases to learn" and came across an article titled "100 Italian phrases to learn." (After extracting the phrases from the HTML, I found out that there were only 96 phrases, but that was still good enough for me.)

The initial app contained the phrases in a JSON file that the frontend loaded. It was completely static, but it worked.

{
"id": 1,
"group": "general",
"translations": {
"en": "Yes",
"it": "Si"
}
}

I put it on Cloudflare Pages and it went live.

I showed it to my girlfriend, but she didn't like some of the phrases I used. If only I had a backend with a database to edit the phrases. Then I had an idea: let's add a database with Supabase.

Supabase is a managed backend solution that provides a lot of free stuff: a PostgreSQL database and social authentication among other things.

Phrase Tutor built with Supabase
Phrase Tutor built with Supabase

I set up the database tables using the Supabase UI which was pretty straightforward.

The table I needed only had a few fields:

CREATE TABLE phrases (
id bigint NOT NULL,
group character varying NULL,
translations_en text NOT NULL,
translations_it text NOT NULL
);

Then I had to seed the database with some SQL. Executing SQL statements is easy with the use of Supabase’s UI. You just log in, open the SQL editor and paste in the code:

INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (1,'general','Yes','Si');
INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (2,'general','No','No');
...

Integrating Supabase into my existing front-end app was simple using their Javascript SDK. If you're familiar with Firebase, it should feel similar. Essentially, you build your SQL queries on the frontend and use the resulting data in your app.

Using the SDK felt pretty straightforward and I could get what I wanted out of the database without much hassle.

const { data, error } = await supabase.from("phrases").select("*");

And just like that, my static Vue.js app had a database to rely on 🎉

Adding the login with Google was a matter of enabling it in Supabase UI and setting up the Client ID and Client Secret variables. In order to trigger the login process with Google, I once again relied on their Javascript SDK.

supabase.auth.signInWithOAuth({ provider: "google" });

Awesome! I'm glad that I can now edit the phrases and that there is a login feature that I plan to use later.

In the future, I have plans to add more languages to the app and also allow registered users to contribute new phrases and translations. I believe this will make the app more useful and engaging for language learners.

And just like that, my app went from a pure static app to an app with a database and Google login 🤯

info

Check out the deployed app written with Vue.js and Supabase: https://phrase-tutor.pages.dev

info

View the source here

Joining Wasp and dogfooding it

Some background before the second part: I started working at Wasp earlier this year. I'm really happy to work on a technology that solves a problem I care about: when I do side-projects, I dislike writing the same dull parts every time from scratch. I copy and paste from my previous side projects, but eventually, the code snippets become old and outdated.

Naturally, I wanted to test out Wasp by rewriting one of my side projects. I decided to see how Wasp could work with the Phrase Tutor project.

Wasp works by having an easy-to-understand config file called main.wasp which coordinates your pieces of client and server functionalities. Its main purpose is to keep you productive and focused on writing interesting bits. It feels pretty much like using a web framework that covers your whole app.

Phrase Tutor built with Wasp
Phrase Tutor built with Wasp

Let's begin by creating the data models. Wasp uses Prisma under the hood to communicate with your database, which makes it easy to manage your database without worrying about the details. This is just one of the many choices the framework made for me, and I appreciate the feeling of using a setup that works.

I had to first declare all of the entities I needed with Prisma PSL in the Wasp config file.

entity Phrase {=psl
id Int @id @default(autoincrement())
group String
phrase String
translations Translation[]
psl=}

entity Language {=psl
id Int @id @default(autoincrement())
name String @unique
emoji String
translations Translation[]
psl=}

entity Translation {=psl
id Int @id @default(autoincrement())
phraseId Int
languageId Int
translation String
phrase Phrase @relation(fields: [phraseId], references: [id], onDelete: Cascade)
language Language @relation(fields: [languageId], references: [id], onDelete: Cascade)
psl=}

I'm using a PostgreSQL database again, and you can see that the field definitions are similar.

I improved the data schema a bit by defining three tables instead of one. I separated the concept of a Phrase from the concepts of Language and Translation. This will make it easier to add new languages in the future.

I added some phrases to the database using Prisma and a Wasp action:

export async function seedItalianPhrases(args, context) {
const data = [
{
id: 1,
group: "general",
translations_en: "Yes",
translations_it: "Si"
},
...
]
for (const phrase of seedPhrases) {
await context.entities.Phrase.create({
...
});
}
}

Let’s now look at what I needed to do to get the data flowing from the backend to my React app.

First, I declared a query in my Wasp config file:

app phraseTutor {
...
}
...

query fetchAllPhrases {
fn: import { getAllPhrases } from "@server/queries.js",
entities: [Phrase]
}

Then I wrote the code for my backend to fetch the phrases. You’ll notice it’s quite similar to the code I wrote for fetching phrases with the Supabase SDK, but I had to include the translations relation since we now have multiple tables.

// My query got the Prisma entity through the context parameter
// which I just used to fetch all the phrases
export async function getAllPhrases(args, context) {
return context.entities.Phrase.findMany({
include: {
translations: true
}
});
}

And lastly, I could just import the query into my React app. It’s set up in a way that it handles cache invalidation automatically, one less thing to worry about, which is awesome 😎

// Wasp relies on React Query in the background
const { data: phrases, isLoading } = useQuery(fetchAllPhrases);

Let’s also add support for Google auth for our app. It involves declaring you want it in the Wasp file, adding some env variables and using it in the React application.

We declare it to the Wasp file by adding the google key under auth:

app phraseTutor {
...
auth: {
userEntity: User,
externalAuthEntity: SocialUser,
methods: {
// Define we want the Google auth
google: {
// Optionally, we can adjust what is saved from the user's data
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/"
},
...
}

// Some of the entities needed for auth
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
profilePicture String
externalAuthAssociations SocialUser[]
createdAt DateTime @default(now())
psl=}

entity SocialUser {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

And … that’s it. We can now use the Google auth in our frontend 🎉

import { signInUrl as googleSignInUrl } from "@wasp/auth/helpers/Google";
...
const { data: user } = useAuth();

Writing a full-stack React and Express.js with Wasp felt like a guided experience; I didn't have to focus too hard on the dev tooling, building, or deploying.

Instead, I could focus on the logic needed for Phrase Tutor to work and just run wasp start most of the time. I did need to write some extra code to get everything running, but I'm free to customize this code however I want.

info

Check out the deployed project built with Wasp: https://phrasetutor.com

info

View the source here

Let's compare some of the features

I want to compare the features of Supabase and Wasp. It's good to think about different ways to do things and their pros and cons.

FeatureSupabaseWasp
Getting data from the APIUse the Supabase JS SDK to query database tablesDeclare query in Wasp config and use Prisma JS SDK to implement it
Custom business logicWriting custom PostgreSQL procedures or by writing edge functionsDeclare actions in the Wasp file and write server-side JS
Defining the database schemaVisual editor or by CREATE TABLE queryBy code - edit Prisma schema and commit changes
AuthEnable in UIEnable it in the Wasp file
DeploymentSupabase managed instance or self-host itDeploy anywhere, support for https://fly.io one line deployment

With Supabase, I liked how familiar the SDK felt and their UI made it easy to configure parts of my backend. I didn’t need to think about deploying Supabase since I used their hosted version, but it did get paused after 1 week of inactivity on the free tier.

On the other hand, Wasp felt like the glue for my React + Express.js + Prisma app and I needed to write more code to get things done. It felt more explicit because I wrote code closer to what I would normally write. I deployed it to fly.io with the Wasp command wasp deploy fly launch and it’s now live on https://phrasetutor.com

Conclusion

It's all about the use case

Choosing the right solution for your needs can be difficult. That's why it's important to try out different options and see how they work for you. In this case, I compared two options: Supabase and Wasp.

Supabase is a great choice if you want a well-rounded open-source BaaS product that adds superpowers to your front-end apps. It provides a lot of free stuff, such as a PostgreSQL database and social authentication, which can make development easier and faster. It also has a nice SDK and UI that the end user can use to easily define their app's configuration.

Wasp is an open-source framework for building full-stack apps that helps out with keeping the boilerplate low. It is a bit more explicit about some things, such as defining your auth entities, but that can be a plus when you have more advanced use cases. By using Wasp as the glue for your full-stack application, you can have the best of both worlds: a development and production setup that works out of the box while still allowing you to develop your app any way you like.

In the case of Phrase Tutor, I liked working with both Supabase and Wasp. I did, however, get a different feeling from working with the two technologies. With Supabase I felt like my front-end app got instant superpowers and it now has a database and login, which was nice considering the effort I had to put in. But now I had a black-box dependency that I needed to build around.

When I used Wasp to rebuild Phrase Tutor, it felt different because it was a full-stack app. I had more control over the application code, so I could change it and evolve it as I wanted. I felt like I had built an app that could grow in any direction. Although I had to write more code, it felt like a good trade-off for future needs.

To decide which option is best for you, I would suggest trying both and seeing how you feel. It is easy to set up both tools and see if they make sense for you.

Grazie for reading 🙃
Grazie for reading 🙃

If you try out the Phrase Tutor app, please let me know what you think. You can reach me on Twitter. I'm always looking for ways to make it better.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html b/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html index 38dfe0f88d..fc664371a5 100644 --- a/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html +++ b/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html @@ -19,13 +19,13 @@ - - + +
-

New React docs pretend SPAs don't exist anymore

· 5 min read
Matija Sosic

Where is Vite

React just released their new docs at https://react.dev/. While it looks great and packs a lot of improvements, one section that caught the community’s attention is “Start a New React Project”. The strongly recommended way to start a new React project is to use a framework such as Next.js, while the traditional route of using bundlers like Vite or CRA is fairly strongly discouraged.

Next.js is a great framework, and its rise in popularity is due in a large part to the return of SEO optimization via Server-Side-Rendering (SSR) within the collective developer conscience. And it definitely does make sense to use a framework that provides SSR for static sites and pages that rely on SEO.

But what about typical Single Page Apps (SPAs)? Dashboard-like tools that live behind the auth (and don’t need SEO at all), and for which React was originally designed, still very much exist.

The new React docs - use a framework unless your app has “unusual” constraints

react new project docs

The new docs make a pretty strong claim for using a framework when starting a new React project. Even if you read through the “Can I use React without a framework” section (hidden behind a collapsed toggle by default), you have to go through a wall of text convincing you why not using a framework is a bad idea, mainly due to the lack of SSR. Only then, in the end, comes the piece mentioning other options, such as Vite and Parcel:

use framework unless you app has unusual constraints

Even then, first you’ll have to admit your app has unusual constraints (and no examples were given of what that could be) before you’re actually “allowed” not to use a framework. It feels very much like you’re doing it in spite of all the warnings and that there actually isn’t a case where you should do it.

Why SPAs (still) matter

SPAs still have their place

SSR/SSG has been getting a lot of attention lately and has been a flagship feature of most new frameworks built on top of React. And rightly so - it has solved a major issue of using React for static & SEO-facing sites where time to first content (FCP) is crucial.

On the other hand, the use case where React, Angular, and other UI frameworks initially shined were dashboard apps (e.g., project management systems, CRMs, …) - it allowed for a radically better UX, which resembled that of desktop apps.

Although interactive content-rich apps (blogging platforms, marketplaces, social platforms) are today a typical poster child demo app for frameworks, dashboard-like apps still very much exist, and there are more of them than ever. Thousands of companies are building their internal tools daily, just like new SaaS-es pop up every day.

SEO is largely irrelevant for them since everything is happening behind the auth layer, where everything is centered around workflows, not content. SSR might even be counter-productive since it puts more pressure on your servers instead of distributing the rendering load across the clients.

How then would you develop SPAs?

Traditionally, React was only a UI library in your stack of choice. You would use CRA (or Vite nowadays) as a bundler/starter for your React project. Then you’d probably add a routing library (e.g., react-router) and maybe a state management library (e.g., Redux, or react-query), and you’d already be set pretty well. You would develop your backend in whatever you choose - Node.js/Express, Rails, or anything else.

There are also new frameworks emerging that focus on this particular use case (e.g., RedwoodJS and Wasp (disclaimer: this is us!)) whose flagship feature is not SSR, but rather the abstraction of API and CRUD on data models, and getting full-stack functionality from UI to the database, with extra features such as easy authentication and deployment out of the box.

With a “go for Next or you are unusual” and “you need SSR” message, React is making a strong signal against other solutions that don’t emphasize SSR as their main feature.

So what’s the big deal? Nobody forces you to use SSR in Next/Remix

That’s correct, but also it’s true that a buy-in into a whole framework is a much bigger step than just opting for a UI library. Frameworks are (more) opinionated and come with many decisions (code structure, architecture, deployment) made upfront for you. Which is great and that’s why they are valuable and why we’ll keep using them.

But, both sides of the story should be presented, and the final call should be left to the developer. React is too useful, valuable, and popular a tool and community to allow itself to skip this step.

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

New React docs pretend SPAs don't exist anymore

· 5 min read
Matija Sosic

Where is Vite

React just released their new docs at https://react.dev/. While it looks great and packs a lot of improvements, one section that caught the community’s attention is “Start a New React Project”. The strongly recommended way to start a new React project is to use a framework such as Next.js, while the traditional route of using bundlers like Vite or CRA is fairly strongly discouraged.

Next.js is a great framework, and its rise in popularity is due in a large part to the return of SEO optimization via Server-Side-Rendering (SSR) within the collective developer conscience. And it definitely does make sense to use a framework that provides SSR for static sites and pages that rely on SEO.

But what about typical Single Page Apps (SPAs)? Dashboard-like tools that live behind the auth (and don’t need SEO at all), and for which React was originally designed, still very much exist.

The new React docs - use a framework unless your app has “unusual” constraints

react new project docs

The new docs make a pretty strong claim for using a framework when starting a new React project. Even if you read through the “Can I use React without a framework” section (hidden behind a collapsed toggle by default), you have to go through a wall of text convincing you why not using a framework is a bad idea, mainly due to the lack of SSR. Only then, in the end, comes the piece mentioning other options, such as Vite and Parcel:

use framework unless you app has unusual constraints

Even then, first you’ll have to admit your app has unusual constraints (and no examples were given of what that could be) before you’re actually “allowed” not to use a framework. It feels very much like you’re doing it in spite of all the warnings and that there actually isn’t a case where you should do it.

Why SPAs (still) matter

SPAs still have their place

SSR/SSG has been getting a lot of attention lately and has been a flagship feature of most new frameworks built on top of React. And rightly so - it has solved a major issue of using React for static & SEO-facing sites where time to first content (FCP) is crucial.

On the other hand, the use case where React, Angular, and other UI frameworks initially shined were dashboard apps (e.g., project management systems, CRMs, …) - it allowed for a radically better UX, which resembled that of desktop apps.

Although interactive content-rich apps (blogging platforms, marketplaces, social platforms) are today a typical poster child demo app for frameworks, dashboard-like apps still very much exist, and there are more of them than ever. Thousands of companies are building their internal tools daily, just like new SaaS-es pop up every day.

SEO is largely irrelevant for them since everything is happening behind the auth layer, where everything is centered around workflows, not content. SSR might even be counter-productive since it puts more pressure on your servers instead of distributing the rendering load across the clients.

How then would you develop SPAs?

Traditionally, React was only a UI library in your stack of choice. You would use CRA (or Vite nowadays) as a bundler/starter for your React project. Then you’d probably add a routing library (e.g., react-router) and maybe a state management library (e.g., Redux, or react-query), and you’d already be set pretty well. You would develop your backend in whatever you choose - Node.js/Express, Rails, or anything else.

There are also new frameworks emerging that focus on this particular use case (e.g., RedwoodJS and Wasp (disclaimer: this is us!)) whose flagship feature is not SSR, but rather the abstraction of API and CRUD on data models, and getting full-stack functionality from UI to the database, with extra features such as easy authentication and deployment out of the box.

With a “go for Next or you are unusual” and “you need SSR” message, React is making a strong signal against other solutions that don’t emphasize SSR as their main feature.

So what’s the big deal? Nobody forces you to use SSR in Next/Remix

That’s correct, but also it’s true that a buy-in into a whole framework is a much bigger step than just opting for a UI library. Frameworks are (more) opinionated and come with many decisions (code structure, architecture, deployment) made upfront for you. Which is great and that’s why they are valuable and why we’ll keep using them.

But, both sides of the story should be presented, and the final call should be left to the developer. React is too useful, valuable, and popular a tool and community to allow itself to skip this step.

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/11/wasp-launch-week-two.html b/blog/2023/04/11/wasp-launch-week-two.html index 484f2726c5..13cfd13a5b 100644 --- a/blog/2023/04/11/wasp-launch-week-two.html +++ b/blog/2023/04/11/wasp-launch-week-two.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #2

· 7 min read
Matija Sosic

Here we go again! After three months of building and talking to our community about what features they'd like to see next, we're proud to kick off our second Launch Week. It stars tomorrow, and you can sign up for the launch event here!

Launch Week 2 is coming

Wasp Beta introduced a lot of core features that enabled developers to a build full-fledged SaaS-es. Since then, our community grew rapidly and we watched you deploy numerous apps and some of you even making their startups and earning their first revenue on top of Wasp!

Seeing that all the essential building blocks are now in place, our next goal became to make Wasp really easy (and fun) to use. We've had a bunch of ideas on everything we'd like to improve with DX for a while, and now finally came the right time to do it.

Nonetheless, the theme and sentiment of this launch week is best captured by an ancient term that poets used to describe some of the most beautiful and marvelous wonders of the world (e.g. pyramids, or the hanging gardens of Babylon): pizzazz 🍕.

Wednesday, Apr 12 - Launch event 🚀 + Pizzazz opener: Auth UI 💅

Wasp's easy auth has been by a long shot one of the most popular features in the community. We decided to take it one step further - Wasp now offers beautifully designed, pre-made auth components that you can simply plug into your app and immediately get that razzle dazzle on!

Auth UI Demo
On your localhost, tomorrow

We'll present this and much more at our Kick-off event, starting tomorrow on our Discord at 10 am EDT / 4 pm CET - sign up here and make sure to mark yourself as interested!

Join us to meet the team and to be the first to get a sneak peek into the latest features! We'll follow up with a casual AMA session, showcase selected community projects and discuss all together about what we'd like to see in Wasp next.

LW2 launch party instructions

P.S. : The word is out that there will be a raffle and that the most lucky one(s) will win some cool Wasp swag! (Da Boi included, ofc).

Thursday, Apr 13 - Deploy your app to Fly.io with a single CLI command

Deploying to Fly.io

When developing your app is blazingly fast, the last thing you want to slow you down is deployment. Figuring out how to exactly setup client/server, dealing with CORS, configuring ports and env vars, ... - well, now you don't have to think about it anymore!

This release of Wasp introduces first CLI deployment helper, for Fly.io (others coming soon, and you're free to contribute)!

How deployment feels now
Deployment in Wasp before vs now

Friday, Apr 14 - Improved database tooling & DX

Database seeding

Introducing two main quality-of-life features here:

  • wasp start db - Fully managed development database - (don't ever run docker run postgres ... again)
  • Database seeding - populate your database with some initial, "seed" data

This was something we ourselves ended up needing often when developing a new app, and although not a huge thing at the first glance, it's feels so good to have it taken care of! Given that Wasp is a fully managed full-stack framework that "understands" all parts of your dev process, we were in unique position to offer this functionality.

P.S. - you haven't been connecting to the prod database all along during development, have you?

Saturday, Apr 15 - More launch goodness: Custom API routes + Email sending ✉️

It's Saturday, so you get two features for the price of one!

Add custom API routes

Custom API routes
Adding a custom route handler at /foo/bar endpoint

Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via operations, sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style.

Email sending: Wasp + Sendgrid/Mailgun/...

Laurence Fishburne messenger pigeons
Don't end up like this, use Wasp for sending emails

Email sending - another feature that sounds like you should be able to implement it in 30 minutes (looking at you, auth), but then you find yourselves a week later cursing web development and having an inexplicable urge to start breeding messenger pigeons (that's what happened to Laurence Fishburne in John Wick, if you ever wondered).

Email sending code example

Wasp offers unified interface for different providers (e.g. Sendgrid or Mailgun, or a custom SMTP server). It also works great with our latest auth method, email - you get email verification and password reset out of the box!

Sunday, Apr 16 - Frontend testing and full-stack type safety!

We continue with our buy-one-get-one-free scheme (although both are free in all fairness):

Frontend testing, powered by Vitest

Frontend testing via Vitest

All you have to do to run your frontend tests is run wasp test client in your CLI! Backed by Vitest, while mocking is powered by MSW and additional Wasp helpers sprinkled on top. Now you really have no excuses to write your tests (except on the backend, support for them is coming next, so enjoy while it lasts)!

Full-stack type safety

Our RPC is now doing serious type-fu
Our typesafe RPC is now doing some serious type-fu

We already introduced glimpses of this in our Beta launch, but now things got even better! Whatever types you define and use on the server, be it entities or your custom types, they immediately get propagated to the client and typecheck in your IDE.

Monday, Apr 17 - SaaS GPT template + Waspathon #2 kick-off!

SaaS GPT template

Aaand we saved the best for the last - we'll put a special highlight on our SaaS GPT starter, which lets you build GPT-powered apps (such as CoverLetterGPT.xyz or SocialPostGPT.xyz) in a day and with all the good stuff pre-included - auth (social, email), Tailwind, deployment, Stripe and GPT API integration, ... - all you need to do is run it and start coding!

Our second hackathon - Waspathon #2!

Hacking away
Hate it when this happens.

And what a better reason to try out the SaaS GPT template than a hackathon! It will be an open format and you're free to build whatever you want - there will be a few categories will grade and award, but more on that coming soon!

The same for the prizes - expect cool wasp-themed swag and useful stuff that makes dev's life easier (no, it doesn't include getting rid of your PM).

We'll share more info and the registration link soon.

Recap

  • We are kicking off Launch Week #2 on Wed, April 12, at 10am EDT / 4pm CET - make sure to register for the event!
  • Launch Week #2 brings a ton of new exciting features - we’ll highlight one each day, starting tomorrow
  • On Monday, April 17, we’ll announce a hackathon - follow us on twitter and join our Discord to stay in the loop!

That’s it, Waspeteers - put your pizzazz (buzzazz?) on and see you tomorrow! 🐝

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #2

· 7 min read
Matija Sosic

Here we go again! After three months of building and talking to our community about what features they'd like to see next, we're proud to kick off our second Launch Week. It stars tomorrow, and you can sign up for the launch event here!

Launch Week 2 is coming

Wasp Beta introduced a lot of core features that enabled developers to a build full-fledged SaaS-es. Since then, our community grew rapidly and we watched you deploy numerous apps and some of you even making their startups and earning their first revenue on top of Wasp!

Seeing that all the essential building blocks are now in place, our next goal became to make Wasp really easy (and fun) to use. We've had a bunch of ideas on everything we'd like to improve with DX for a while, and now finally came the right time to do it.

Nonetheless, the theme and sentiment of this launch week is best captured by an ancient term that poets used to describe some of the most beautiful and marvelous wonders of the world (e.g. pyramids, or the hanging gardens of Babylon): pizzazz 🍕.

Wednesday, Apr 12 - Launch event 🚀 + Pizzazz opener: Auth UI 💅

Wasp's easy auth has been by a long shot one of the most popular features in the community. We decided to take it one step further - Wasp now offers beautifully designed, pre-made auth components that you can simply plug into your app and immediately get that razzle dazzle on!

Auth UI Demo
On your localhost, tomorrow

We'll present this and much more at our Kick-off event, starting tomorrow on our Discord at 10 am EDT / 4 pm CET - sign up here and make sure to mark yourself as interested!

Join us to meet the team and to be the first to get a sneak peek into the latest features! We'll follow up with a casual AMA session, showcase selected community projects and discuss all together about what we'd like to see in Wasp next.

LW2 launch party instructions

P.S. : The word is out that there will be a raffle and that the most lucky one(s) will win some cool Wasp swag! (Da Boi included, ofc).

Thursday, Apr 13 - Deploy your app to Fly.io with a single CLI command

Deploying to Fly.io

When developing your app is blazingly fast, the last thing you want to slow you down is deployment. Figuring out how to exactly setup client/server, dealing with CORS, configuring ports and env vars, ... - well, now you don't have to think about it anymore!

This release of Wasp introduces first CLI deployment helper, for Fly.io (others coming soon, and you're free to contribute)!

How deployment feels now
Deployment in Wasp before vs now

Friday, Apr 14 - Improved database tooling & DX

Database seeding

Introducing two main quality-of-life features here:

  • wasp start db - Fully managed development database - (don't ever run docker run postgres ... again)
  • Database seeding - populate your database with some initial, "seed" data

This was something we ourselves ended up needing often when developing a new app, and although not a huge thing at the first glance, it's feels so good to have it taken care of! Given that Wasp is a fully managed full-stack framework that "understands" all parts of your dev process, we were in unique position to offer this functionality.

P.S. - you haven't been connecting to the prod database all along during development, have you?

Saturday, Apr 15 - More launch goodness: Custom API routes + Email sending ✉️

It's Saturday, so you get two features for the price of one!

Add custom API routes

Custom API routes
Adding a custom route handler at /foo/bar endpoint

Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via operations, sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style.

Email sending: Wasp + Sendgrid/Mailgun/...

Laurence Fishburne messenger pigeons
Don't end up like this, use Wasp for sending emails

Email sending - another feature that sounds like you should be able to implement it in 30 minutes (looking at you, auth), but then you find yourselves a week later cursing web development and having an inexplicable urge to start breeding messenger pigeons (that's what happened to Laurence Fishburne in John Wick, if you ever wondered).

Email sending code example

Wasp offers unified interface for different providers (e.g. Sendgrid or Mailgun, or a custom SMTP server). It also works great with our latest auth method, email - you get email verification and password reset out of the box!

Sunday, Apr 16 - Frontend testing and full-stack type safety!

We continue with our buy-one-get-one-free scheme (although both are free in all fairness):

Frontend testing, powered by Vitest

Frontend testing via Vitest

All you have to do to run your frontend tests is run wasp test client in your CLI! Backed by Vitest, while mocking is powered by MSW and additional Wasp helpers sprinkled on top. Now you really have no excuses to write your tests (except on the backend, support for them is coming next, so enjoy while it lasts)!

Full-stack type safety

Our RPC is now doing serious type-fu
Our typesafe RPC is now doing some serious type-fu

We already introduced glimpses of this in our Beta launch, but now things got even better! Whatever types you define and use on the server, be it entities or your custom types, they immediately get propagated to the client and typecheck in your IDE.

Monday, Apr 17 - SaaS GPT template + Waspathon #2 kick-off!

SaaS GPT template

Aaand we saved the best for the last - we'll put a special highlight on our SaaS GPT starter, which lets you build GPT-powered apps (such as CoverLetterGPT.xyz or SocialPostGPT.xyz) in a day and with all the good stuff pre-included - auth (social, email), Tailwind, deployment, Stripe and GPT API integration, ... - all you need to do is run it and start coding!

Our second hackathon - Waspathon #2!

Hacking away
Hate it when this happens.

And what a better reason to try out the SaaS GPT template than a hackathon! It will be an open format and you're free to build whatever you want - there will be a few categories will grade and award, but more on that coming soon!

The same for the prizes - expect cool wasp-themed swag and useful stuff that makes dev's life easier (no, it doesn't include getting rid of your PM).

We'll share more info and the registration link soon.

Recap

  • We are kicking off Launch Week #2 on Wed, April 12, at 10am EDT / 4pm CET - make sure to register for the event!
  • Launch Week #2 brings a ton of new exciting features - we’ll highlight one each day, starting tomorrow
  • On Monday, April 17, we’ll announce a hackathon - follow us on twitter and join our Discord to stay in the loop!

That’s it, Waspeteers - put your pizzazz (buzzazz?) on and see you tomorrow! 🐝

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/12/auth-ui.html b/blog/2023/04/12/auth-ui.html index 94bf61e337..114afba249 100644 --- a/blog/2023/04/12/auth-ui.html +++ b/blog/2023/04/12/auth-ui.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Auth UI: The first full-stack auth with self-updating forms!

· 2 min read
Matija Sosic

One of the main benefits of Wasp is having deep understanding of your entire full-stack app - e.g. what routes you have, what data models you defined, but also what methods you use for authentication. And that enables us to do some pretty cool stuff for you!

Auth UI Demo
Customize auth forms to fit your brand!

Once you've listed auth methods you want to use in your .wasp config file, you're done - from that Wasp generates a full authentication form that you simply import as a React component. And the best part is that is updates dynamically as you add/remove auth providers!

You can see the docs and give it a try here.

Auto-updating magic 🔮

Auth UI Demo gif
Add GitHub as another auth provider -> the form updates automatically!

tip

Since .wasp config file contains a high-level description of your app's requirements, Wasp can deduce a lot of stuff for you from it, and this is just a single example.

When you update your .wasp file by adding/removing an auth method (GitHub in this case), Wasp will detect it and automatically regenerate the auth form. No need to configure anything else, or change your React code - just a single line change in .wasp file and everything else will get taken care of!

Mind exploding
When you realize Wasp is a compiler and actually understands your app 🤯

Customize it! 🎨

Although it looks nice, all of this wouldn't be really useful if you couldn't customize it to fit your brand. That's easily done through the component's props:

Customizing auth form through props
Easily customize your auth form through props!

And that's it! You can see the whole list of tokens you can customize here. More are coming in the future!

Wasp out 🐝 🎤- give it a try and let us know how you liked it in our Discord !

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Auth UI: The first full-stack auth with self-updating forms!

· 2 min read
Matija Sosic

One of the main benefits of Wasp is having deep understanding of your entire full-stack app - e.g. what routes you have, what data models you defined, but also what methods you use for authentication. And that enables us to do some pretty cool stuff for you!

Auth UI Demo
Customize auth forms to fit your brand!

Once you've listed auth methods you want to use in your .wasp config file, you're done - from that Wasp generates a full authentication form that you simply import as a React component. And the best part is that is updates dynamically as you add/remove auth providers!

You can see the docs and give it a try here.

Auto-updating magic 🔮

Auth UI Demo gif
Add GitHub as another auth provider -> the form updates automatically!

tip

Since .wasp config file contains a high-level description of your app's requirements, Wasp can deduce a lot of stuff for you from it, and this is just a single example.

When you update your .wasp file by adding/removing an auth method (GitHub in this case), Wasp will detect it and automatically regenerate the auth form. No need to configure anything else, or change your React code - just a single line change in .wasp file and everything else will get taken care of!

Mind exploding
When you realize Wasp is a compiler and actually understands your app 🤯

Customize it! 🎨

Although it looks nice, all of this wouldn't be really useful if you couldn't customize it to fit your brand. That's easily done through the component's props:

Customizing auth form through props
Easily customize your auth form through props!

And that's it! You can see the whole list of tokens you can customize here. More are coming in the future!

Wasp out 🐝 🎤- give it a try and let us know how you liked it in our Discord !

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/13/db-start-and-seed.html b/blog/2023/04/13/db-start-and-seed.html index 976a9463ef..28969297f7 100644 --- a/blog/2023/04/13/db-start-and-seed.html +++ b/blog/2023/04/13/db-start-and-seed.html @@ -19,13 +19,13 @@ - - + +
-

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

· 6 min read
Martin Sosic

As a full-stack framework, Wasp doesn’t care “just” about frontend and backend, but it also covers the database!

It does this by allowing you to define Prisma data models in a Wasp file, connecting them to the relevant Wasp Operations, warning you if you need to do database migrations, deploying the database for you (if you choose so), … .

Since Wasp knows so much about your database, that puts us in a good position to keep finding ways to improve the developer experience regarding dealing with the database. For Wasp v0.10, we focused on:

  1. Wasp running the dev database for you with no config needed → Fully Managed Dev Database 🚀
  2. Wasp helping you to initialize the database with some data → Db Seeding 🌱

strong wasp database
Wasp now has `wasp start db` and `wasp db seed`!

Fully Managed Dev Database 🚀

You might have asked yourself:

If Wasp already knows so much about my database, why do I need to bother running it on my own!?

Ok, when you start a new Wasp project it is easy because you are using an SQLite database, but once you switch to Postgres, it falls onto you to take care of it: run it, provide its URL to Wasp via env var, handle multiple databases if you have multiple Wasp apps, … .

This can get tedious quickly, especially if you are visiting your Wasp project that you haven’t worked on for a bit and need to figure out again how to run the db, or you need to check out somebody else’s Wasp project and don’t have it all set up yet. It is something most of us are used to, especially with other frameworks, but still, we can do better at Wasp!

This is where wasp start db comes in!

wasp start db running in terminal
wasp start db in action, running a posgtres dev db for you

Now, all you need to do to run the development database, is run wasp start db, and Wasp will run it for you and will know how to connect to it during development.

No env var setting, no remembering how to run the db. The only requirement is that you have Docker installed on your machine. Data from your database will be persisted on the disk between the runs, and each Wasp app will have its own database assigned.

Btw, you can still use a custom database that you ran on your own if you want, the same way it was done before in Wasp: by setting env var DATABASE_URL.

Database seeding 🌱

Database seeding is a term for populating the database with some initial data.

Seeding is most commonly used for two following scenarios:

  1. To put the development database into a state convenient for testing / playing with it.
  2. To initialize the dev/staging/prod database with some essential data needed for it to be useful, for example, default currencies in a Currency table.

Wasp so far had no direct support for seeding, so you had to either come up with your own solution (e.g. script that connects to the db and executes some queries), or massage data manually via Prisma Studio (wasp db studio).

There is one big drawback to both of the approaches I mentioned above though: there is no easy way to reuse logic that you have already implemented in your Wasp app, especially Actions (e.g. createTask)! This is pretty bad, as it makes your seeding logic brittle.

This is where wasp db seed comes in! Now, Wasp allows you to write a JS/TS function, import any server logic (including Actions) into it as you wish, and then seed the database with it.

wasp db seed running in terminal
wasp db seed in action, initializing the db with dev data

Registering seed functions in Wasp is easy:

app MyApp {
// ...
db: {
// ...
seeds: [
import { devSeedSimple } from "@server/dbSeeds.js",
import { prodSeed } from "@server/dbSeeds.js"
]
}
}

Example of a seed function from above, devSeedSimple:

import { createTask } from './actions.js'

export const devSeedSimple = async (prismaClient) => {
const user = await createUser(prismaClient, {
username: "RiuTheDog",
password: "bark1234"
})

await createTask(
{ description: "Chase the cat" },
{ user, entities: { Task: prismaClient.task } }
)
}

async function createUser (prismaClient, data) {
const { password, ...newUser } = await prismaClient.user.create({ data })
return newUser
}

Finally, to run these seeds, you can either do:

  • wasp db seed: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run.
  • wasp db seed <seed-name>: It will run the seed function with the specified name, where the name is the identifier you used in its import expression in the app.db.seeds list. Example: wasp db seed devSeedSimple.

We also added wasp db reset command (calls prisma db reset in the background) that cleans up the database for you (removes all data and tables and re-applies migrations), which is great to use in combination with wasp db seed, as a precursor.

Plans for the future 🔮

  • allow customization of managed dev database (Postgres plugins, custom Dockerfile, …)
  • have Wasp run the managed dev database automatically whenever it needs it (instead of you having to run wasp start db manually)
  • dynamically find a free port for managed dev database (right now it requires port 5432)
  • provide utility functions to make writing seeding functions easier (e.g. functions for creating new users)
  • right now seeding functions are defined as part of a Wasp server code → it might be interesting to separate them in a standalone “project” in the future, while still keeping their easy access to the server logic.
  • do you have any ideas/suggestions? Let us know in our Discord !
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

· 6 min read
Martin Sosic

As a full-stack framework, Wasp doesn’t care “just” about frontend and backend, but it also covers the database!

It does this by allowing you to define Prisma data models in a Wasp file, connecting them to the relevant Wasp Operations, warning you if you need to do database migrations, deploying the database for you (if you choose so), … .

Since Wasp knows so much about your database, that puts us in a good position to keep finding ways to improve the developer experience regarding dealing with the database. For Wasp v0.10, we focused on:

  1. Wasp running the dev database for you with no config needed → Fully Managed Dev Database 🚀
  2. Wasp helping you to initialize the database with some data → Db Seeding 🌱

strong wasp database
Wasp now has `wasp start db` and `wasp db seed`!

Fully Managed Dev Database 🚀

You might have asked yourself:

If Wasp already knows so much about my database, why do I need to bother running it on my own!?

Ok, when you start a new Wasp project it is easy because you are using an SQLite database, but once you switch to Postgres, it falls onto you to take care of it: run it, provide its URL to Wasp via env var, handle multiple databases if you have multiple Wasp apps, … .

This can get tedious quickly, especially if you are visiting your Wasp project that you haven’t worked on for a bit and need to figure out again how to run the db, or you need to check out somebody else’s Wasp project and don’t have it all set up yet. It is something most of us are used to, especially with other frameworks, but still, we can do better at Wasp!

This is where wasp start db comes in!

wasp start db running in terminal
wasp start db in action, running a posgtres dev db for you

Now, all you need to do to run the development database, is run wasp start db, and Wasp will run it for you and will know how to connect to it during development.

No env var setting, no remembering how to run the db. The only requirement is that you have Docker installed on your machine. Data from your database will be persisted on the disk between the runs, and each Wasp app will have its own database assigned.

Btw, you can still use a custom database that you ran on your own if you want, the same way it was done before in Wasp: by setting env var DATABASE_URL.

Database seeding 🌱

Database seeding is a term for populating the database with some initial data.

Seeding is most commonly used for two following scenarios:

  1. To put the development database into a state convenient for testing / playing with it.
  2. To initialize the dev/staging/prod database with some essential data needed for it to be useful, for example, default currencies in a Currency table.

Wasp so far had no direct support for seeding, so you had to either come up with your own solution (e.g. script that connects to the db and executes some queries), or massage data manually via Prisma Studio (wasp db studio).

There is one big drawback to both of the approaches I mentioned above though: there is no easy way to reuse logic that you have already implemented in your Wasp app, especially Actions (e.g. createTask)! This is pretty bad, as it makes your seeding logic brittle.

This is where wasp db seed comes in! Now, Wasp allows you to write a JS/TS function, import any server logic (including Actions) into it as you wish, and then seed the database with it.

wasp db seed running in terminal
wasp db seed in action, initializing the db with dev data

Registering seed functions in Wasp is easy:

app MyApp {
// ...
db: {
// ...
seeds: [
import { devSeedSimple } from "@server/dbSeeds.js",
import { prodSeed } from "@server/dbSeeds.js"
]
}
}

Example of a seed function from above, devSeedSimple:

import { createTask } from './actions.js'

export const devSeedSimple = async (prismaClient) => {
const user = await createUser(prismaClient, {
username: "RiuTheDog",
password: "bark1234"
})

await createTask(
{ description: "Chase the cat" },
{ user, entities: { Task: prismaClient.task } }
)
}

async function createUser (prismaClient, data) {
const { password, ...newUser } = await prismaClient.user.create({ data })
return newUser
}

Finally, to run these seeds, you can either do:

  • wasp db seed: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run.
  • wasp db seed <seed-name>: It will run the seed function with the specified name, where the name is the identifier you used in its import expression in the app.db.seeds list. Example: wasp db seed devSeedSimple.

We also added wasp db reset command (calls prisma db reset in the background) that cleans up the database for you (removes all data and tables and re-applies migrations), which is great to use in combination with wasp db seed, as a precursor.

Plans for the future 🔮

  • allow customization of managed dev database (Postgres plugins, custom Dockerfile, …)
  • have Wasp run the managed dev database automatically whenever it needs it (instead of you having to run wasp start db manually)
  • dynamically find a free port for managed dev database (right now it requires port 5432)
  • provide utility functions to make writing seeding functions easier (e.g. functions for creating new users)
  • right now seeding functions are defined as part of a Wasp server code → it might be interesting to separate them in a standalone “project” in the future, while still keeping their easy access to the server logic.
  • do you have any ideas/suggestions? Let us know in our Discord !
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/17/How-I-Built-CoverLetterGPT.html b/blog/2023/04/17/How-I-Built-CoverLetterGPT.html index cc777e606e..394316adc7 100644 --- a/blog/2023/04/17/How-I-Built-CoverLetterGPT.html +++ b/blog/2023/04/17/How-I-Built-CoverLetterGPT.html @@ -19,13 +19,13 @@ - - + +
-

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

· 3 min read
Vinny


Like many other software developers, I enjoy trying out new technologies even if it's just to get a feel for what they can do.

So when I first learned about the OpenAI API, I knew I wanted to give it a try. I had already wanted to create a SaaS app that could help manage the process of applying to numerous jobs, and the prospect of adding GPT into the mix made it even more interesting. So with API access and a bit of free time, I decided to give it a shot.

I threw together a simple version of the app in about 3-4 days and CoverLetterGPT was born, a SaaS app that uses GPT-3.5-turbo to generate, revise, and manage cover letters for you based on your skills and the specific job descriptions.

Even though I did think it had potential as a SaaS app, I was approaching it mostly as a way to learn how to build one for the first time. And after seeing so many people "building in public" and sharing their progress, I thought it would be fun to try it out myself.

Hey peeps. Check out http://coverlettergpt.xyz. You can try it out now and create your own cover letters for free (no Payment/API key). I'm working on A LOT more features. Stay Tuned!

So I started sharing my progress on Twitter, Reddit, and Indie Hackers. I made my first post about it on March 9th, and because I was just experimenting and trying my hand at a SaaS app for the first time, I also open-sourced the app to share the code and what I was learning with others. This led to a lot of interest and great feedback, and I ended up getting featured in the indiehackers newsletter, which led to even more interest.

Within the first month, I got over 1,000 sign-ups along with my first paying customers. Pretty surprising, to say the least!

So to continue in the spirit of curiosity, learning, and just "wingin' it," I decided to make a code walkthrough video that explains how I built the app, the tools I used to build it, and a little bit about how I marketed the app without spending any money.

As an extra bonus, I also give a quick introduction to the free SaaS template I created for building your own SaaS app, with or without GPT, on the PERN stack (PostgreSQL/Prisma, Express, React, NodeJS).

My hope is that others will learn something from my experience, and that it could inspire them to try out new technologies and build that app idea they've had in mind (and if they do, they should make sure to share it with me on Twitter @hot_town -- I'd love to see it!)

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

· 3 min read
Vinny


Like many other software developers, I enjoy trying out new technologies even if it's just to get a feel for what they can do.

So when I first learned about the OpenAI API, I knew I wanted to give it a try. I had already wanted to create a SaaS app that could help manage the process of applying to numerous jobs, and the prospect of adding GPT into the mix made it even more interesting. So with API access and a bit of free time, I decided to give it a shot.

I threw together a simple version of the app in about 3-4 days and CoverLetterGPT was born, a SaaS app that uses GPT-3.5-turbo to generate, revise, and manage cover letters for you based on your skills and the specific job descriptions.

Even though I did think it had potential as a SaaS app, I was approaching it mostly as a way to learn how to build one for the first time. And after seeing so many people "building in public" and sharing their progress, I thought it would be fun to try it out myself.

Hey peeps. Check out http://coverlettergpt.xyz. You can try it out now and create your own cover letters for free (no Payment/API key). I'm working on A LOT more features. Stay Tuned!

So I started sharing my progress on Twitter, Reddit, and Indie Hackers. I made my first post about it on March 9th, and because I was just experimenting and trying my hand at a SaaS app for the first time, I also open-sourced the app to share the code and what I was learning with others. This led to a lot of interest and great feedback, and I ended up getting featured in the indiehackers newsletter, which led to even more interest.

Within the first month, I got over 1,000 sign-ups along with my first paying customers. Pretty surprising, to say the least!

So to continue in the spirit of curiosity, learning, and just "wingin' it," I decided to make a code walkthrough video that explains how I built the app, the tools I used to build it, and a little bit about how I marketed the app without spending any money.

As an extra bonus, I also give a quick introduction to the free SaaS template I created for building your own SaaS app, with or without GPT, on the PERN stack (PostgreSQL/Prisma, Express, React, NodeJS).

My hope is that others will learn something from my experience, and that it could inspire them to try out new technologies and build that app idea they've had in mind (and if they do, they should make sure to share it with me on Twitter @hot_town -- I'd love to see it!)

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/27/wasp-hackathon-two.html b/blog/2023/04/27/wasp-hackathon-two.html index fda7beae41..93119113a4 100644 --- a/blog/2023/04/27/wasp-hackathon-two.html +++ b/blog/2023/04/27/wasp-hackathon-two.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Hackathon #2 - Let's "hack-a-ton"!

· 2 min read
Vinny


So Launch Week #2 has officially come to an end, and as the tradition goes, the end of the launch week means the beginning of a hackathon!

We've launched a ton of new features for you to build your Hackathon project with, including:

You can read all it in this blog post, or watch a 1-minute video showing how it all works in practice 🎬!

Launch Week #2 Features -- YouTube Short
Launch Week #2 Features -- YouTube Short

Even better, we've got a new starter templates feature that lets you create a new project with a pre-built template, so you can get started even faster! Like this sweet SaaS template with GPT, Stripe, SendGrid, and Tailwind UI already integrated:

Wasp SaaS Template w/ GPT, Stripe, and more 🎊

Just run wasp new my-project -t saas and you're good to go.

The prizes for the hackathon include an awesome Wasp-themed mechanical keyboard, tons of Wasp swag, and more cool stuff (e.g., virtual hugs from the team)!

The only rule is to use Wasp, and you can build whatever you want (but both you and I know it's going to be a GPT-powered app, so make sure to use our template).

The applications are open, and the hackathon starts on April 28th and ends May 7th. You can apply (solo or with a team) here:


Good luck and Happy Hacking 🐝🚀!



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Hackathon #2 - Let's "hack-a-ton"!

· 2 min read
Vinny


So Launch Week #2 has officially come to an end, and as the tradition goes, the end of the launch week means the beginning of a hackathon!

We've launched a ton of new features for you to build your Hackathon project with, including:

You can read all it in this blog post, or watch a 1-minute video showing how it all works in practice 🎬!

Launch Week #2 Features -- YouTube Short
Launch Week #2 Features -- YouTube Short

Even better, we've got a new starter templates feature that lets you create a new project with a pre-built template, so you can get started even faster! Like this sweet SaaS template with GPT, Stripe, SendGrid, and Tailwind UI already integrated:

Wasp SaaS Template w/ GPT, Stripe, and more 🎊

Just run wasp new my-project -t saas and you're good to go.

The prizes for the hackathon include an awesome Wasp-themed mechanical keyboard, tons of Wasp swag, and more cool stuff (e.g., virtual hugs from the team)!

The only rule is to use Wasp, and you can build whatever you want (but both you and I know it's going to be a GPT-powered app, so make sure to use our template).

The applications are open, and the hackathon starts on April 28th and ends May 7th. You can apply (solo or with a team) here:


Good luck and Happy Hacking 🐝🚀!



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/05/19/hackathon-2-review.html b/blog/2023/05/19/hackathon-2-review.html index 1bd7ceadd3..cebaa9ef69 100644 --- a/blog/2023/05/19/hackathon-2-review.html +++ b/blog/2023/05/19/hackathon-2-review.html @@ -19,13 +19,13 @@ - - + +
-

Hackathon #2: Results & Review

· 6 min read
Vinny

To finalize Wasp's Launch Week #2, we held our second Hackathon. Just like the "Betathon" before it, it was an open hackathon where the only requirement was to build something cool with Wasp!

In this post, I’ll give a quick overview of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Typergotchi

Typergotchi

Our unofficial mascot, Da Boi, makes his debut app appearance in this fun, feature-rich typing game!

Wasp makes building fullstack apps fast and fun. We've done lots of hackthons in the past, and we couldn't pass up the chance to win a mechanical keyboard :)” - Umbrien & kg04ls

🥈 Office Wars

Office Wars

A turn-based, multi-player strategy game where you command your tank across a hexagonal map. A great way to keep your coworkers engaged while you wait for your code to compile!

We love how Wasp brings the tools that are already being used by developers under the same umbrella. It's very streamlined and makes building fullstack apps easy to accomplish... like django but w/ more superpowers” - Roland & Luís

🥉 Tied for Third: Bee Pretty & StorAI

Bee Pretty

StorAI

After 5 minutes of working with Wasp I thought, this is phenomenal! So much just works out of the box -- everything was flawless" - mkinkela1

🥳 And A Big Round of Applause for the Rest of the Participants!

Thanks so much to rest of the participants:

  • Max for submitting Feedback Hub, which we award "the most SaaS-y app".
  • Richard for submitting Promise, for winning the "best last-minute minimal-effort submission" award.
  • Swarnavo for submitting his Dashboard Panel app.

Hackathon How-to

For our first hackathon, the "Betathon", we announced and started it on the final day of our launch week. Looking back, this probably wasn't the best approach because it didn't give people much time to prepare. This time around, we announced the hackathon a week in advance, giving people a bit more time to prepare their projects.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & countdown timer

And just like last time, we kept the Hackathon rules simple: no categories, no constraints, just 10 days to create any fullstack web app using Wasp, alone or in a team of up to 4 people.

Keyboard

We may be a bit unoriginal here, but we also decided to offer the same grand prize as the Betathon: a Wasp-colored mechanical keyboard. On top of that, runner-ups also got some project-related prizes, as well as Wasp beanies, shirts, and other swag. Of course, we also spotlight the winner’s on our social media accounts.

Something new we did this time was hold a post-hackathon presentation event on Discord, thanks to a suggestion made by Max, one of our most dedicated contributers. We gave each team a chance to present their projects and talk a bit about their experience. The turnout was great, with almost all the teams participating, and it helped us to get to know the faces behind the apps. Not only was this a great way to connect more with our community, but it also gave us some insight into where are users are coming from, what they're interested in, and what they're looking for in Wasp.

Promotion

As of late, we've made an effort to promote exemplary apps built with Wasp, as well as create some of our own. This has been a great way to show off Wasp's capabilities, and has resulted in a noticeable increase in interest and traffic. Therefore, for the Hackathon, we let the organic interest in Wasp be the driver for the Hackathon, as we didn't do much promotion outside of our own channels, nor did we partner with any other sponsors this time. We simply announced the Hackathon and directed people to our Hacakthon homepage we created.

The hackathon page is nice to have as a central spot for all the rules and relevant info. We also added a fun intro video using AI-generated narration of a possibly well-known actor 😎. Overall, the effort put into the homepage gives participants the feeling that they’re entering into a serious contest and committing to something of substance, while the light-heartedness of the promotion material lets them know that it's more about fun than serious prizes. But even in the abscence of big winnings, the quality of the submissions were suprisingly high. Intrinsic motivation, ftw! 🤩

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

Again, just like we did previously, we wrote the Hackathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response 2.0

We were really pleased to see the response to the Hackathon surpass our expectations, yet again. The number, quality, and creativity of the submissions were even better than the Betathon. We also had a lot of fun interacting with the participants, and we're looking forward to doing it again soon.

It's reaffirming to see Wasp grow along with our community, as they build more and more cool stuff with it. Events like this give us a morale and confidence boost as it confirms that we're building something the community wants.

Thanks so much again to the participants for their hard work and contributions. We're grateful and happy to have you along for the ride! 🐝🚀

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hackathon #2: Results & Review

· 6 min read
Vinny

To finalize Wasp's Launch Week #2, we held our second Hackathon. Just like the "Betathon" before it, it was an open hackathon where the only requirement was to build something cool with Wasp!

In this post, I’ll give a quick overview of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Typergotchi

Typergotchi

Our unofficial mascot, Da Boi, makes his debut app appearance in this fun, feature-rich typing game!

Wasp makes building fullstack apps fast and fun. We've done lots of hackthons in the past, and we couldn't pass up the chance to win a mechanical keyboard :)” - Umbrien & kg04ls

🥈 Office Wars

Office Wars

A turn-based, multi-player strategy game where you command your tank across a hexagonal map. A great way to keep your coworkers engaged while you wait for your code to compile!

We love how Wasp brings the tools that are already being used by developers under the same umbrella. It's very streamlined and makes building fullstack apps easy to accomplish... like django but w/ more superpowers” - Roland & Luís

🥉 Tied for Third: Bee Pretty & StorAI

Bee Pretty

StorAI

After 5 minutes of working with Wasp I thought, this is phenomenal! So much just works out of the box -- everything was flawless" - mkinkela1

🥳 And A Big Round of Applause for the Rest of the Participants!

Thanks so much to rest of the participants:

  • Max for submitting Feedback Hub, which we award "the most SaaS-y app".
  • Richard for submitting Promise, for winning the "best last-minute minimal-effort submission" award.
  • Swarnavo for submitting his Dashboard Panel app.

Hackathon How-to

For our first hackathon, the "Betathon", we announced and started it on the final day of our launch week. Looking back, this probably wasn't the best approach because it didn't give people much time to prepare. This time around, we announced the hackathon a week in advance, giving people a bit more time to prepare their projects.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & countdown timer

And just like last time, we kept the Hackathon rules simple: no categories, no constraints, just 10 days to create any fullstack web app using Wasp, alone or in a team of up to 4 people.

Keyboard

We may be a bit unoriginal here, but we also decided to offer the same grand prize as the Betathon: a Wasp-colored mechanical keyboard. On top of that, runner-ups also got some project-related prizes, as well as Wasp beanies, shirts, and other swag. Of course, we also spotlight the winner’s on our social media accounts.

Something new we did this time was hold a post-hackathon presentation event on Discord, thanks to a suggestion made by Max, one of our most dedicated contributers. We gave each team a chance to present their projects and talk a bit about their experience. The turnout was great, with almost all the teams participating, and it helped us to get to know the faces behind the apps. Not only was this a great way to connect more with our community, but it also gave us some insight into where are users are coming from, what they're interested in, and what they're looking for in Wasp.

Promotion

As of late, we've made an effort to promote exemplary apps built with Wasp, as well as create some of our own. This has been a great way to show off Wasp's capabilities, and has resulted in a noticeable increase in interest and traffic. Therefore, for the Hackathon, we let the organic interest in Wasp be the driver for the Hackathon, as we didn't do much promotion outside of our own channels, nor did we partner with any other sponsors this time. We simply announced the Hackathon and directed people to our Hacakthon homepage we created.

The hackathon page is nice to have as a central spot for all the rules and relevant info. We also added a fun intro video using AI-generated narration of a possibly well-known actor 😎. Overall, the effort put into the homepage gives participants the feeling that they’re entering into a serious contest and committing to something of substance, while the light-heartedness of the promotion material lets them know that it's more about fun than serious prizes. But even in the abscence of big winnings, the quality of the submissions were suprisingly high. Intrinsic motivation, ftw! 🤩

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

Again, just like we did previously, we wrote the Hackathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response 2.0

We were really pleased to see the response to the Hackathon surpass our expectations, yet again. The number, quality, and creativity of the submissions were even better than the Betathon. We also had a lot of fun interacting with the participants, and we're looking forward to doing it again soon.

It's reaffirming to see Wasp grow along with our community, as they build more and more cool stuff with it. Events like this give us a morale and confidence boost as it confirms that we're building something the community wants.

Thanks so much again to the participants for their hard work and contributions. We're grateful and happy to have you along for the ride! 🐝🚀

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/07/wasp-beta-update-may-23.html b/blog/2023/06/07/wasp-beta-update-may-23.html index d6141ca4be..02c0fe12ae 100644 --- a/blog/2023/06/07/wasp-beta-update-may-23.html +++ b/blog/2023/06/07/wasp-beta-update-may-23.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta - May 2023

· 6 min read
Matija Sosic

Wasp Update May 23

Want to stay in the loop? → Join our newsletter!

Hola Waspeteers 🐝,

What did one plant say to the other? Aloe! Long thyme no see. 🌱

Now that we've set the tone, let me guide you through what's new in Waspworld (that would be a cool theme park, right?):

Wasp Hackathon 2.0 is over - congrats to the winners! 🐝 🏆 🐝

Congrats to the hackathon winners!
Shoutout to the winning team - Typergotchi! They even made a cool illustration with our mascot, Da Boi 🐝 😎

We had more submissions than ever, and the quality and creativity of your apps were really at the next level. We had everything from admin dashboards and GPT-powered story-telling apps to the actual games.

Hackathon testimonial

See all the winners and read a full Hackathon 2.0 review 👉 here 👈.

Wasp Launch Week #3 is in the making - get ready for the Magic 🔮 🧙

As it always happens in the wilderness, after one launch week, there comes another one. And who are we to defy the laws of nature - thus, get ready for Launch Week #3!

We are aiming for the end of June, but we'll announce the exact date soon. Make sure to follow us on Twitter or/and join our Discord to stay in the loop.

Beautiful
When you see it ✨

After Pizzazz 🍕 ...

As you might remember, the motto/topic of our last launch was Pizzaz, which referred to improving the developer experience in Wasp - full-stack auth, one-line deployment, type safety, db tooling, ...

... Comes Magic! 🔮

While DX will always be our top priority, we're now shifting gears a bit - the keyword we chose to represent our next launch is ✨ Magic ✨. The reason is that now that we have a majority of the features you'd expect in a web framework in place, we can start utilizing Wasp's unique compiler-driven approach to offer next-level features no other framework can!

LW3 Sneak Peek 🤫 👀

More details coming soon, but in the meanwhile, here are some of the features we're most excited about:

🚧 Wasp AI 🤖 ✨

There is no mAgIc without AI! We cannot share many details on this yet, but it is something we've been exploring a lot lately. Our previous experiments have shown that, due to its declarative and human-readable nature, Wasp is naturally a very good fit for LLMs.

We'll take this to the next level for our next launch - stay tuned!

🚧 Auto CRUD

Although Wasp helps a lot with bootstrapping your app, one repetitive thing that you have to do every time is implement "standard" CRUD operations for your data models.

We decided to put a stop to it - welcome our new (incoming) feature, Auto CRUD!

Auto CRUD
Syntax proposal for the new Auto CRUD feature

All you have to do is specify in your .wasp file which CRUD operations you want, and they will be auto-generated for you to use in your JS/TS code. The best part is when you update your data model, these will get updated as well! 🤯

This feature is also a really good showcase of Wasp's compiler muscles - the best you could get with a traditional framework approach is scaffolding, which means spitting out code that will quickly get outdated and that you have to maintain.

See a 2-min demo of Wasp Auto CRUD in action - by our founding engineer Miho

Showing off compiler muscles
Our compiler right now

🚧 Advanced syntax completion for .wasp files (LSP)

Improved LSP

We're making our VS Code extension even better! So far it has provided highlighting and auto-completion for top-level declarations (e.g., route, entity, query, ...), but now it's going even deeper. Every property will display its full type as you are typing it out + you'll get a context-aware auto-completion.

🚧 Support for web sockets 🔌 🧦!

Wasp will soon support Web Sockets! This will allow you to have a persistent, real-time connection between your client and server, which is great for chat apps, games, and more.

Web sockets in Wasp
Defining a new web socket in Wasp config file

For now it is a stand-alone feature, but it opens some really interesting possibilities - e.g. combining this with Wasp's query/action system and letting you declare a particular query to be "live". Just an idea for now but something to keep in mind as we test and receive more feedback on this feature.

From the blog 📖

The community buzz 🐝 💬

Last month was super buzzy! We got several awesome reviews, and Wasp also got picked up by a couple of YouTube dev influencers:

Wasp testimonial

Wasp GitHub Star Growth - 2,825 ⭐

Getting close to the big 3,000! Huge thanks to all our contributors and stargazers - you are amazing!

GitHub stars - almost 3,000!
Almost 3,000 stars! 🐝 🚀

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! 🌯

A dramatic goodbye gif
A dramatic goodbye - don't ever let go

That's it for this month and thanks for reading! Since you've come this far, you deserve one final treat - a Wasp-themed joke generated by ChatGPT:

GPT Wasp joke
Good one, dad.

Fly high, and we'll see you soon 🐝 🐝,
+

Wasp Beta - May 2023

· 6 min read
Matija Sosic

Wasp Update May 23

Want to stay in the loop? → Join our newsletter!

Hola Waspeteers 🐝,

What did one plant say to the other? Aloe! Long thyme no see. 🌱

Now that we've set the tone, let me guide you through what's new in Waspworld (that would be a cool theme park, right?):

Wasp Hackathon 2.0 is over - congrats to the winners! 🐝 🏆 🐝

Congrats to the hackathon winners!
Shoutout to the winning team - Typergotchi! They even made a cool illustration with our mascot, Da Boi 🐝 😎

We had more submissions than ever, and the quality and creativity of your apps were really at the next level. We had everything from admin dashboards and GPT-powered story-telling apps to the actual games.

Hackathon testimonial

See all the winners and read a full Hackathon 2.0 review 👉 here 👈.

Wasp Launch Week #3 is in the making - get ready for the Magic 🔮 🧙

As it always happens in the wilderness, after one launch week, there comes another one. And who are we to defy the laws of nature - thus, get ready for Launch Week #3!

We are aiming for the end of June, but we'll announce the exact date soon. Make sure to follow us on Twitter or/and join our Discord to stay in the loop.

Beautiful
When you see it ✨

After Pizzazz 🍕 ...

As you might remember, the motto/topic of our last launch was Pizzaz, which referred to improving the developer experience in Wasp - full-stack auth, one-line deployment, type safety, db tooling, ...

... Comes Magic! 🔮

While DX will always be our top priority, we're now shifting gears a bit - the keyword we chose to represent our next launch is ✨ Magic ✨. The reason is that now that we have a majority of the features you'd expect in a web framework in place, we can start utilizing Wasp's unique compiler-driven approach to offer next-level features no other framework can!

LW3 Sneak Peek 🤫 👀

More details coming soon, but in the meanwhile, here are some of the features we're most excited about:

🚧 Wasp AI 🤖 ✨

There is no mAgIc without AI! We cannot share many details on this yet, but it is something we've been exploring a lot lately. Our previous experiments have shown that, due to its declarative and human-readable nature, Wasp is naturally a very good fit for LLMs.

We'll take this to the next level for our next launch - stay tuned!

🚧 Auto CRUD

Although Wasp helps a lot with bootstrapping your app, one repetitive thing that you have to do every time is implement "standard" CRUD operations for your data models.

We decided to put a stop to it - welcome our new (incoming) feature, Auto CRUD!

Auto CRUD
Syntax proposal for the new Auto CRUD feature

All you have to do is specify in your .wasp file which CRUD operations you want, and they will be auto-generated for you to use in your JS/TS code. The best part is when you update your data model, these will get updated as well! 🤯

This feature is also a really good showcase of Wasp's compiler muscles - the best you could get with a traditional framework approach is scaffolding, which means spitting out code that will quickly get outdated and that you have to maintain.

See a 2-min demo of Wasp Auto CRUD in action - by our founding engineer Miho

Showing off compiler muscles
Our compiler right now

🚧 Advanced syntax completion for .wasp files (LSP)

Improved LSP

We're making our VS Code extension even better! So far it has provided highlighting and auto-completion for top-level declarations (e.g., route, entity, query, ...), but now it's going even deeper. Every property will display its full type as you are typing it out + you'll get a context-aware auto-completion.

🚧 Support for web sockets 🔌 🧦!

Wasp will soon support Web Sockets! This will allow you to have a persistent, real-time connection between your client and server, which is great for chat apps, games, and more.

Web sockets in Wasp
Defining a new web socket in Wasp config file

For now it is a stand-alone feature, but it opens some really interesting possibilities - e.g. combining this with Wasp's query/action system and letting you declare a particular query to be "live". Just an idea for now but something to keep in mind as we test and receive more feedback on this feature.

From the blog 📖

The community buzz 🐝 💬

Last month was super buzzy! We got several awesome reviews, and Wasp also got picked up by a couple of YouTube dev influencers:

Wasp testimonial

Wasp GitHub Star Growth - 2,825 ⭐

Getting close to the big 3,000! Huge thanks to all our contributors and stargazers - you are amazing!

GitHub stars - almost 3,000!
Almost 3,000 stars! 🐝 🚀

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! 🌯

A dramatic goodbye gif
A dramatic goodbye - don't ever let go

That's it for this month and thanks for reading! Since you've come this far, you deserve one final treat - a Wasp-themed joke generated by ChatGPT:

GPT Wasp joke
Good one, dad.

Fly high, and we'll see you soon 🐝 🐝,
Matija, Martin and the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/06/22/wasp-launch-week-three.html b/blog/2023/06/22/wasp-launch-week-three.html index 618f4f8ed4..5c9a889aba 100644 --- a/blog/2023/06/22/wasp-launch-week-three.html +++ b/blog/2023/06/22/wasp-launch-week-three.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #3: Magic

· 6 min read
Matija Sosic

Launch Week 3 is coming

By now, it is a tradition. For the every upcoming launch week, we ask ourselves how can we top the last one? How can we make building full-stack web apps easier, more enjoyable and get rid of even more boilerplate?

If this is the first time you're joining, check our previous launches:

Our first launch week was about making the promise of Wasp Alpha a reality, so you can build what you envisioned and deploy your app to production. The second launch made the whole experience much more polished, getting closer to the DX you'd expect from a mature web framework.

Why Magic?

For this launch, with all the basics in place and you having built thousands of apps with Wasp (thank you!), we started pushing the boundaries of what web frameworks can do, utilising Wasp's unique DSL/compiler approach. This is still barely scratching the surface, but you'll be able to try it out yourself and get a taste of what the future of web development will look like.

Magic - LW3 in a nutshell
This launch week in a nutshell.

What's coming 🐝

Every day next week, starting Monday, June 26, we'll highlight a major new feature in Wasp. We'll update this post daily as we reveal each feature, so make sure to keep coming back! Follow us on twitter (@wasplang) to stay in the loop and also join our Discord to join the community and get help as you're trying Wasp out.

Launch party 🚀🎉

launch event 2 - screenshot
A bit of the atmosphere from our last launch party

What would a launch be without a proper event and a party? A boring, heartless event, that's what!

That's why we'll get together to celebrate the launch, our community (you!) and all the hard work that's been put into this new, fresh edition of Wasp. You will also get to meet the team and hear first-hand from the makers about the latest features and plans for the future.

The party starts at 9.30 am EDT / 3.30 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

As per usual, there will be memes, swag and lots of interesting dev discussions!

Auto CRUD | Monday: The future is now 🛸

The future is now

We'll immediately kick things off with a bang! What's the one thing that all developers universally agree is something they'd like to do less of? Writing boilerplate CRUD logic, of course! Yet, it's 2023 and the best we managed to do is get an AI write it half-correctly for us and we still have to maintain it.

That's what we are coming after - is it possible to avoid writing (or generating) CRUD code in the first place? How far can we take it and what's then even left for your to code? Join us on Monday and find out!

When: Monday, June 26 2023

Read more about it:

WebSocket Support | Tuesday: Be real, time 🔌⏱

Realtime

Sometimes, you just want to keep it real. Especially when you are dealing with time. I've been dropping some hints here - have you figured out what is this about? If yes, drop us a line on twitter (@wasplang) and the first one to get it right will get a special (real and timely) award!

Another situation where you might want to keep things real is when chatting to someone, especially via the text (wink wink hint hint 🧦).

When: Tuesday, June 27 2023

Read more about it:

Wednesday: Community Day 🤗

Community
Just let it all out

Community is at the centre of Wasp, and Wednesday is at the centre of the week, so it's only appropriate to marry the two together. We'll spotlight the amazing OSS tools Wasp is built on top of and also you - all the cool stuff you have built with Wasp and how you're contributing every day to make our community better!

When: Wednesday, June 28 2023

Read more about it: What can you build with Wasp?

Wasp LSP 2.0 | Thursday: Take care of your tools 🛠

Tools

It's a well known fact that a developer is only as good as the tools they are using. That actually applies to anybody - if Gimli hadn't spent time sharpening his axe, he wouldn't stand a chance against these orcs, would he?

Us at Wasp, we are pretty much the same as Gimli - we take our tools seriously. As we are innovating on the framework features, our goal is to do the same with the tooling you use with Wasp. Get ready to get your hands dirty (with code).

When: Thursday, June 29 2023

Read more about it: A blog post introing Wasp LSP 2.0

GPT Web App Generator | Friday: Waspularity 🤖 + Tutorial-o-thon!

Waspularity

For the final day of the launch week, we have a really cool surprise for you. I'll just say it's something like Matrix but the robots are your friends and there's no that weird guy with sunglasses to ruin everything. And there might be cake.

To wrap the week up, we'll also start another hackathon, but this time in a bit different format. Since the best way to learn something is to teach it to others, we'll focus on tutorials this time! May the best tutorial win - more info coming soon.

When: Friday, June 30 2023

Read more about it:

Recap

  • We are kicking off Launch Week #3 on Mon, June 26, at 9.30am EDT / 3.30pm CET - make sure to register for the event!
  • Launch Week #3 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a tutorial-o-thon - get your writing gear ready!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #3: Magic

· 6 min read
Matija Sosic

Launch Week 3 is coming

By now, it is a tradition. For the every upcoming launch week, we ask ourselves how can we top the last one? How can we make building full-stack web apps easier, more enjoyable and get rid of even more boilerplate?

If this is the first time you're joining, check our previous launches:

Our first launch week was about making the promise of Wasp Alpha a reality, so you can build what you envisioned and deploy your app to production. The second launch made the whole experience much more polished, getting closer to the DX you'd expect from a mature web framework.

Why Magic?

For this launch, with all the basics in place and you having built thousands of apps with Wasp (thank you!), we started pushing the boundaries of what web frameworks can do, utilising Wasp's unique DSL/compiler approach. This is still barely scratching the surface, but you'll be able to try it out yourself and get a taste of what the future of web development will look like.

Magic - LW3 in a nutshell
This launch week in a nutshell.

What's coming 🐝

Every day next week, starting Monday, June 26, we'll highlight a major new feature in Wasp. We'll update this post daily as we reveal each feature, so make sure to keep coming back! Follow us on twitter (@wasplang) to stay in the loop and also join our Discord to join the community and get help as you're trying Wasp out.

Launch party 🚀🎉

launch event 2 - screenshot
A bit of the atmosphere from our last launch party

What would a launch be without a proper event and a party? A boring, heartless event, that's what!

That's why we'll get together to celebrate the launch, our community (you!) and all the hard work that's been put into this new, fresh edition of Wasp. You will also get to meet the team and hear first-hand from the makers about the latest features and plans for the future.

The party starts at 9.30 am EDT / 3.30 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

As per usual, there will be memes, swag and lots of interesting dev discussions!

Auto CRUD | Monday: The future is now 🛸

The future is now

We'll immediately kick things off with a bang! What's the one thing that all developers universally agree is something they'd like to do less of? Writing boilerplate CRUD logic, of course! Yet, it's 2023 and the best we managed to do is get an AI write it half-correctly for us and we still have to maintain it.

That's what we are coming after - is it possible to avoid writing (or generating) CRUD code in the first place? How far can we take it and what's then even left for your to code? Join us on Monday and find out!

When: Monday, June 26 2023

Read more about it:

WebSocket Support | Tuesday: Be real, time 🔌⏱

Realtime

Sometimes, you just want to keep it real. Especially when you are dealing with time. I've been dropping some hints here - have you figured out what is this about? If yes, drop us a line on twitter (@wasplang) and the first one to get it right will get a special (real and timely) award!

Another situation where you might want to keep things real is when chatting to someone, especially via the text (wink wink hint hint 🧦).

When: Tuesday, June 27 2023

Read more about it:

Wednesday: Community Day 🤗

Community
Just let it all out

Community is at the centre of Wasp, and Wednesday is at the centre of the week, so it's only appropriate to marry the two together. We'll spotlight the amazing OSS tools Wasp is built on top of and also you - all the cool stuff you have built with Wasp and how you're contributing every day to make our community better!

When: Wednesday, June 28 2023

Read more about it: What can you build with Wasp?

Wasp LSP 2.0 | Thursday: Take care of your tools 🛠

Tools

It's a well known fact that a developer is only as good as the tools they are using. That actually applies to anybody - if Gimli hadn't spent time sharpening his axe, he wouldn't stand a chance against these orcs, would he?

Us at Wasp, we are pretty much the same as Gimli - we take our tools seriously. As we are innovating on the framework features, our goal is to do the same with the tooling you use with Wasp. Get ready to get your hands dirty (with code).

When: Thursday, June 29 2023

Read more about it: A blog post introing Wasp LSP 2.0

GPT Web App Generator | Friday: Waspularity 🤖 + Tutorial-o-thon!

Waspularity

For the final day of the launch week, we have a really cool surprise for you. I'll just say it's something like Matrix but the robots are your friends and there's no that weird guy with sunglasses to ruin everything. And there might be cake.

To wrap the week up, we'll also start another hackathon, but this time in a bit different format. Since the best way to learn something is to teach it to others, we'll focus on tutorials this time! May the best tutorial win - more info coming soon.

When: Friday, June 30 2023

Read more about it:

Recap

  • We are kicking off Launch Week #3 on Mon, June 26, at 9.30am EDT / 3.30pm CET - make sure to register for the event!
  • Launch Week #3 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a tutorial-o-thon - get your writing gear ready!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/27/build-your-own-twitter-agent-langchain.html b/blog/2023/06/27/build-your-own-twitter-agent-langchain.html index bc166c62cb..83f2435a5b 100644 --- a/blog/2023/06/27/build-your-own-twitter-agent-langchain.html +++ b/blog/2023/06/27/build-your-own-twitter-agent-langchain.html @@ -19,13 +19,13 @@ - - + +
-

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

· 46 min read
Vinny

TL;DR

LangChain, ChatGPT, and other emerging technology have made it possible to build some really creative tools.

In this tutorial, we’ll build a full-stack web app that acts as our own personal Twitter Agent, or “intern”, as I like to call it. It keeps track of your notes and ideas, and uses them — along with tweets from trending-setting twitter users — to brainstorm new ideas and write tweet drafts for you! 💥

BTW, If you get stuck during the tutorial, or at any point just want to check out the full, final repo of the app we're building, here it is: https://github.com/vincanger/twitter-intern

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built in compiler that lets you build your app in a day and deploy with a single CLI command.

We’re working hard to help you build performant web apps as easily as possibly — including making these tutorials, which are released weekly!

We would be super grateful if you could help us out by starring our repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

https://media2.giphy.com/media/d0Pkp9OMIBdC0/giphy.gif?cid=7941fdc6b39mgj7h8orvi0f4bjebceyx4gj0ih1xb6s05ujc&ep=v1_gifs_search&rid=giphy.gif&ct=g

…even Ron would star Wasp on GitHub 🤩

Background

Twitter is a great marketing tool. It’s also a great way to explore ideas and refine your own. But it can be time-consuming and difficult to maintain a tweeting habit.

https://media0.giphy.com/media/WSrR5xkvljaFMe7UPo/giphy.gif?cid=7941fdc6g9o3drj567dbwyuo1c66x76eq8awc2r1oop8oypl&ep=v1_gifs_search&rid=giphy.gif&ct=g

That’s why I decided to build my own personal twitter agent with LangChain on the basis of these assumptions:

🧠 LLMs (like ChatGPT) aren’t the best writers, but they ARE great at brainstorming new ideas.

📊 Certain twitter users drive the majority of discourse within certain niches, i.e. trend-setters influence what’s being discussed at the moment.

💡 the Agent needs context in order to generate ideas relevant to YOU and your opinions, so it should have access to your notes, ideas, tweets, etc.

So instead of trying to build a fully autonomous agent that does the tweeting for you, I thought it would be better to build an agent that does the BRAINSTORMING for you, based on your favorite trend-setting twitter users as well as your own ideas.

Imagine it like an intern that does the grunt work, while you do the curating!

https://media.giphy.com/media/26DNdV3b6dqn1jzR6/giphy.gif

In order to accomplish this, we need to take advantage of a few hot AI tools:

  • Embeddings and Vector Databases
  • LLMs (Large Language Models), such as ChatGPT
  • LangChain and sequential “chains” of LLM calls

Embeddings and Vector Databases give us a powerful way to perform similarity searches on our own notes and ideas.

If you’re not familiar with similarity search, the simplest way to describe what similarity search is by comparing it to a normal google search. In a normal search, the phrase “a mouse eats cheese” will return results with a combination of those words only. But a vector-based similarity search, on the other hand, would return those words, as well as results with related words such as “dog”, “cat”, “bone”, and “fish”.

You can see why that’s so powerful, because if we have non-exact but related notes, our similarity search will still return them!

https://media2.giphy.com/media/xUySTD7evBn33BMq3K/giphy.gif?cid=7941fdc6273if8qfk83gbnv8uabc4occ0tnyzk0g0gfh0qg5&ep=v1_gifs_search&rid=giphy.gif&ct=g

For example, if our favorite trend-setting twitter user makes a post about the benefits of typescript, but we only have a note on “our favorite React hooks”, our similarity search would still likely return such a result. And that’s huge!

Once we get those notes, we can pass them to the ChatGPT completion API along with a prompt to generate more ideas. The result from this prompt will then be sent to another prompt with instructions to generate a draft tweet. We save these sweet results to our Postgres relational database.

This “chain” of prompting is essentially where the LangChain package gets its name 🙂

The flow of information through the app

This approach will give us a wealth of new ideas and tweet drafts related to our favorite trend-setting twitter users’ tweets. We can look through these, edit and save our favorite ideas to our “notes” vector store, or maybe send off some tweets.

I’ve personally been using this app for a while now, and not only has it generated some great ideas, but it also helps to inspire new ones (even if some of the ideas it generates are “meh”), which is why I included an “Add Note” feature front and center to the nav bar

twitter-agent-add-note.png

Ok. Enough background. Let’s start building your own personal twitter intern! 🤖

BTW, if you get stuck at all while following the tutorial, you can always reference this tutorial’s repo, which has the finished app: Twitter Intern GitHub Repo

Configuration

Set up your Wasp project

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

# First, install Wasp by running this in your terminal:

curl -sSL https://get.wasp-lang.dev/installer.sh | sh

# next, create a new project:

wasp new twitter-agent

# cd into the new directory and start the project:

cd twitter-agent && wasp start

Great! When running wasp start, Wasp will install all the necessary npm packages, start our server on port 3001, and our React client on port 3000. Head to localhost:3000 in your browser to check it out.

Untitled

Tip ℹ️

you can install the Wasp vscode extension for the best developer experience.

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s start adding some server-side code.

Server-Side & Database Entities

Start by adding a .env.server file in the root directory of your project:

# https://platform.openai.com/account/api-keys
OPENAI_API_KEY=

# sign up for a free tier account at https://www.pinecone.io/
PINECONE_API_KEY=
# will be a location, e.g 'us-west4-gcp-free'
PINECONE_ENV=

# We will fill these in later during the Twitter Scraping section
# Twitter details -- only needed once for Rettiwt.account.login() to get the tokens
TWITTER_EMAIL=
TWITTER_HANDLE=
TWITTER_PASSWORD=

# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT=
TWID=
CT0=
AUTH_TOKEN=

We need a way for us to store all our great ideas. So let’s first head to Pinecone.io and set up a free trial account.

Untitled

In the Pinecone dashboard, go to API keys and create a new one. Copy and paste your Environment and API Key into .env.server

Do the same for OpenAI, by creating an account and key at https://platform.openai.com/account/api-keys

Now let’s replace the contents of the main.wasp config file, which is like the “skeleton” of your app, with the code below. This will configure most of the fullstack app for you 🤯

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
head: [
"<script async src='https://platform.twitter.com/widgets.js' charset='utf-8'></script>"
],
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// ### Database Models

entity Tweet {=psl
id Int @id @default(autoincrement())
tweetId String
authorUsername String
content String
tweetedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
drafts TweetDraft[]
ideas GeneratedIdea[]
psl=}

entity TweetDraft {=psl
id Int @id @default(autoincrement())
content String
notes String
originalTweet Tweet @relation(fields: [originalTweetId], references: [id])
originalTweetId Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

entity GeneratedIdea {=psl
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
originalTweet Tweet? @relation(fields: [originalTweetId], references: [id])
originalTweetId Int?
isEmbedded Boolean @default(false)
psl=}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
createdAt DateTime @default(now())
favUsers String[]
originalTweets Tweet[]
tweetDrafts TweetDraft[]
generatedIdeas GeneratedIdea[]
psl=}

// <<< Client Pages & Routes

route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true,
component: import Main from "@client/MainPage"
}

//...
note

You might have noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)

With this, our app structure is mostly defined and Wasp will take care of a ton of configuration for us.

Database Setup

But we still need to get a postgres database running. Usually this can be pretty annoying, but with Wasp, just have Docker Deskop installed and running, then open up another separate terminal tab/window and then run:

wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just leave this terminal tab, along with docker desktop, open and running in the background.

In a different terminal tab, run:

wasp db migrate-dev

and make sure to give your database migration a name.

If you stopped the wasp dev server to run this command, go ahead and start it again with wasp start.

At this point, our app will be navigating us to localhost:3000/login but because we haven’t implemented a login screen/flow yet, we will be seeing a blank screen. Don’t worry, we’ll get to that.

Embedding Ideas & Notes

Server Action

First though, in the main.wasp config file, let’s define a server action for saving notes and ideas. Go ahead and add the code below to the bottom of the file:

// main.wasp

//...
// <<< Client Pages & Routes

route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true,
component: import Main from "@client/MainPage"
}

// !!! Actions

action embedIdea {
fn: import { embedIdea } from "@server/ideas.js",
entities: [GeneratedIdea]
}

With the action declared, let’s create it. Make a new file, .src/server/ideas.ts in and add the following code:

import type { EmbedIdea } from '@wasp/actions/types';
import type { GeneratedIdea } from '@wasp/entities';
import HttpError from '@wasp/core/HttpError.js';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

export const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

/**
* Embeds a single idea into the vector store
*/
export const embedIdea: EmbedIdea<{ idea: string }, GeneratedIdea> = async ({ idea }, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

console.log('idea: ', idea);

try {
let newIdea = await context.entities.GeneratedIdea.create({
data: {
content: idea,
userId: context.user.id,
},
});


if (!newIdea) {
throw new HttpError(404, 'Idea not found');
}

const pinecone = await initPinecone();

// we need to create an index to save the vector embeddings to
// an index is similar to a table in relational database world
const availableIndexes = await pinecone.listIndexes();
if (!availableIndexes.includes('embeds-test')) {
console.log('creating index');
await pinecone.createIndex({
createRequest: {
name: 'embeds-test',
// open ai uses 1536 dimensions for their embeddings
dimension: 1536,
},
});
}

const pineconeIndex = pinecone.Index('embeds-test');

// the LangChain vectorStore wrapper
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: context.user.username,
});

// create a document with the idea's content to be embedded
const ideaDoc = new Document({
metadata: { type: 'note' },
pageContent: newIdea.content,
});

// add the document to the vectore store along with its id
await vectorStore.addDocuments([ideaDoc], [newIdea.id.toString()]);

newIdea = await context.entities.GeneratedIdea.update({
where: {
id: newIdea.id,
},
data: {
isEmbedded: true,
},
});
console.log('idea embedded successfully!', newIdea);
return newIdea;
} catch (error: any) {
throw new Error(error);
}
};
info

We’ve defined the action function in our main.wasp file as coming from ‘@server/ideas.js’ but we’re creating an ideas.ts file. What's up with that?!

Well, Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). It does not apply to client files.

Great! Now we have a server action for adding notes and ideas to our vector database. And we didn’t even have to configure a server ourselves (thanks, Wasp 🙂).

Let's take a step back and walk through the code we just wrote though:

  1. We create a new Pinecone client and initialize it with our API key and environment.
  2. We create a new OpenAIEmbeddings client and initialize it with our OpenAI API key.
  3. We create a new index in our Pinecone database to store our vector embeddings.
  4. We create a new PineconeStore, which is a LangChain wrapper around our Pinecone client and our OpenAIEmbeddings client.
  5. We create a new Document with the idea’s content to be embedded.
  6. We add the document to the vector store along with its id.
  7. We also update the idea in our Postgres database to mark it as embedded.

Now we want to create the client-side functionality for adding ideas, but you’ll remember we defined an auth object in our wasp config file. So we’ll need to add the ability to log in before we do anything on the frontend.

Authentication

Let’s add that quickly by adding a new a Route and Page definition to our main.wasp file

//...

route LoginPageRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/LoginPage"
}

…and create the file src/client/LoginPage.tsx with the following content:

import { LoginForm } from '@wasp/auth/forms/Login';
import { SignupForm } from '@wasp/auth/forms/Signup';
import { useState } from 'react';

export default () => {
const [showSignupForm, setShowSignupForm] = useState(false);

const handleShowSignupForm = () => {
setShowSignupForm((x) => !x);
};

return (
<>
{showSignupForm ? <SignupForm /> : <LoginForm />}
<div onClick={handleShowSignupForm} className='underline cursor-pointer hover:opacity-80'>
{showSignupForm ? 'Already Registered? Login!' : 'No Account? Sign up!'}
</div>
</>
);
};
info

In the auth object on the main.wasp file, we used the usernameAndPassword method which is the simplest form of auth Wasp offers. If you’re interested, Wasp does provide abstractions for Google, Github, and Email Verified Authentication, but we will stick with the simplest auth for this tutorial.

With authentication all set up, if we try to go to localhost:3000 we will be automatically directed to the login/register form.

You’ll see that Wasp creates Login and Signup forms for us because of the auth object we defined in the main.wasp file. Sweet! 🎉

But even though we’ve added some style classes, we haven’t set up any css styling so it will probably be pretty ugly right about now.

🤢 Barf.

Untitled

Adding Tailwind CSS

Luckily, Wasp comes with tailwind css support, so all we have to do to get that working is add the following files in the root directory of the project:

.
├── main.wasp
├── src
│ ├── client
│ ├── server
│ └── shared
├── postcss.config.cjs # add this file here
├── tailwind.config.cjs # and this here too
└── .wasproot

postcss.config.cjs

module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

Finally, replace the contents of your src/client/Main.css file with these lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now we’ve got the magic of tailwind css on our sides! 🎨 We’ll get to styling later though. Patience, young grasshopper.

Adding Notes Client-side

From here, let’s create the complimentary client-side components for adding notes to the vector store. Create a new .src/client/AddNote.tsx file with the following contents:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<button
onClick={handleEmbedIdea}
className='flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 font-bold px-3 py-1 text-sm text-blue-500 whitespace-nowrap rounded-lg'
>
{isIdeaEmbedding ? 'Loading...' : 'Save Note'}
</button>
</div>
);
}

Here we’re using the embedIdea action we defined earlier to add our idea to the vector store. We’re also using the useState hook to keep track of the idea we’re adding, as well as the loading state of the button.

So now we have a way to add our own ideas and notes to our vector store. Pretty sweet!

Generating New Ideas & Tweet Drafts

Using LangChain's Sequential Chains

Now we need to set up the sequential chain of LLM calls that LangChain is so great at.

Here are the steps we will take:

  1. define a function that uses LangChain to initiate a “chain” of API calls to OpenAI’s ChatGPT completions endpoint.
    1. this function takes a tweet that we pulled from one of our favorite twitter users as an argument, searches our vector store for similar notes & ideas, and returns a list of new “brainstormed” based on the example tweet and our notes.
  2. define a new action that loops through our favorite users array, pulls their most recent tweets, and sends them to our LangChain function mentioned above

So let’s start again by creating our LangChain function. Make a new src/server/chain.ts file:

import { ChatOpenAI } from 'langchain/chat_models/openai';
import { LLMChain, SequentialChain } from 'langchain/chains';
import { PromptTemplate } from 'langchain/prompts';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

export const generateIdeas = async (exampleTweet: string, username: string) => {
try {
// remove quotes and curly braces as not to confuse langchain template parser
exampleTweet = exampleTweet.replace(/"/g, '');
exampleTweet = exampleTweet.replace(/{/g, '');
exampleTweet = exampleTweet.replace(/}/g, '');

const pinecone = await initPinecone();

console.log('list indexes', await pinecone.listIndexes());

// find the index we created earlier
const pineconeIndex = pinecone.Index('embeds-test');

const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: username,
});

//
// sequential tweet chain begin --- >
//
/**
* vector store results for notes similar to the original tweet
*/
const searchRes = await vectorStore.similaritySearchWithScore(exampleTweet, 2);
console.log('searchRes: ', searchRes);
let notes = searchRes
.filter((res) => res[1] > 0.7) // filter out strings that have less than %70 similarity
.map((res) => res[0].pageContent)
.join(' ');

console.log('\n\n similarity search results of our notes-> ', notes);

if (!notes || notes.length <= 2) {
notes = exampleTweet;
}

const tweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 0.8, // 0 - 2 with 0 being more deterministic and 2 being most "loose". Past 1.3 the results tend to be more incoherent.
modelName: 'gpt-3.5-turbo',
});

const tweetTemplate = `You are an expert idea generator. You will be given a user's notes and your goal is to use this information to brainstorm other novel ideas.

Notes: {notes}

Ideas Brainstorm:
-`;

const tweetPromptTemplate = new PromptTemplate({
template: tweetTemplate,
inputVariables: ['notes'],
});

const tweetChain = new LLMChain({
llm: tweetLlm,
prompt: tweetPromptTemplate,
outputKey: 'newTweetIdeas',
});

const interestingTweetTemplate = `You are an expert interesting tweet generator. You will be given some tweet ideas and your goal is to choose one, and write a tweet based on it. Structure the tweet in an informal yet serious tone and do NOT include hashtags in the tweet!

Tweet Ideas: {newTweetIdeas}

Interesting Tweet:`;

const interestingTweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 1.1,
modelName: 'gpt-3.5-turbo',
});

const interestingTweetPrompt = new PromptTemplate({
template: interestingTweetTemplate,
inputVariables: ['newTweetIdeas'],
});

const interestingTweetChain = new LLMChain({
llm: interestingTweetLlm,
prompt: interestingTweetPrompt,
outputKey: 'interestingTweet',
});

const overallChain = new SequentialChain({
chains: [tweetChain, interestingTweetChain],
inputVariables: ['notes'],
outputVariables: ['newTweetIdeas', 'interestingTweet'],
verbose: false,
});

type ChainDraftResponse = {
newTweetIdeas: string;
interestingTweet: string;
notes: string;
};

const res1 = (await overallChain.call({
notes,
})) as ChainDraftResponse;

return {
...res1,
notes,
};
} catch (error: any) {
throw new Error(error);
}
};

Great! Let's run through the above code real quick:

  1. Initialize the Pinecone client
  2. Find our pinecone index (i.e. table) that we created earlier and initialize a new PineconeStore with LangChain
  3. Search our vector store for notes similar to the example tweet, filtering out any results that have less than %70 similarity
  4. Create a new ChatGPT completion chain that takes our notes as input and generates new tweet ideas
  5. Create a new ChatGPT completion chain that takes the new tweet ideas as input and generates a new tweet draft
  6. Create a new SequentialChain and combine the above two chains together so that we can pass it our notes as input and it returns the new tweet ideas and the new tweet draft as output
VECTOR COSINE SIMILARITY SCORES

A good similarity threshold for cosine similarity search on text strings depends on the specific application and the desired level of strictness in matching. Cosine similarity scores range between 0 and 1, with 0 meaning no similarity and 1 meaning completely identical text strings.

  • 0.8-0.9 = strict
  • 0.6-0.8 = moderate
  • 0.5 = relaxed.

In our case, we went for a moderate similarity threshold of 0.7, which means that we will only return notes that are at least 70% similar to the example tweet.

With this function, we will get our newTweetIdeas and our interestingTweet draft back as results that we can use within our server-side action.

Scraping Twitter

Before we can pass an exampleTweet as an argument to our newly created Sequential Chain, we need to fetch it first!

To do this, we're going to use the Rettiwt-Api (which is just Twitter written backwards). Because it's an unofficial API there are a few caveats:

  1. We have to use the rettiwt client to login to our twitter account once. We will output the tokens it returns via a script and save those in our .env.server file for later.
  2. It's best to use an alternative account for this process. If you don't have an alternative account, go ahead and register a new one now.
⚠️

The use of an unofficial Twitter client, Rettiwt, is for illustrative purposes only. It's crucial that you familiarize yourself with Twitter's policies and rules regarding scraping before implementing these methods. Any abuse or misuse of these scripts and techniques may lead to actions taken against your Twitter account. We hold no responsibility for any consequences arising from your personal use of this tutorial and/or the related scripts. It is intended purely for learning and educational purposes.

Let's go ahead and create a new folder in src/server called scripts with a file inside called tokens.ts. This will be our script that we will run only once, just so that we get the necessary tokens to pass to our Rettiwt client.

We want to avoid running this script many times otherwise our account could get rate-limited. This shouldn't be an issue though, because once we return the tokens, they are valid for up to a year.

So inside src/server/scripts/tokens.ts add the following code:

import { Rettiwt } from 'rettiwt-api'; 

/**
* This is a script we can now run from the cli with `wasp db seed`
* IMPORTANT! We only want to run this script once, after which we save the tokens
* in the .env.server file. They should be good for up to a year.
*/
export const getTwitterTokens = async () => {
const tokens = await Rettiwt().account.login(
process.env.TWITTER_EMAIL!,
process.env.TWITTER_HANDLE!,
process.env.TWITTER_PASSWORD!
);

console.log('tokens: ', tokens)
};

Make sure to add your twitter login details to our .env.server file, if you haven't already!

Great. To be able to run this script via a simple Wasp CLI command, add it via the seeds array within the db object at the top of your main.wasp file:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
//...
db: {
system: PostgreSQL,
seeds: [ // <---------- add this
import { getTwitterTokens } from "@server/scripts/tokens.js",
]
},
//...

Nice! Now for the fun part :)

in your terminal, at the root of your project, run wasp db seed, and you should see the tokens output to the terminal similar to this:

[Db]      Running seed: getTwitterTokens
[Db] tokens: { // your tokens... }

Copy and paste those tokens into your .env.server file:


# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT='...'
TWID='...'
CT0='...'
AUTH_TOKEN='...'

Now with that, we should be able to access our favorite trend-setting users' recent tweets and use them to help us brainstorm new ideas!

Server Action

Ok, so we've got the tokens we need to get our trend-setting example tweets, and we've got a function that runs our similarity search and sequential chain of LLM calls.

Now let’s define an action in our main.wasp file that pulls it all together:

// actions...

action generateNewIdeas {
fn: import { generateNewIdeas } from "@server/ideas.js",
entities: [GeneratedIdea, Tweet, TweetDraft, User]
}

…and then create that action within src/server/ideas.ts


import type {
EmbedIdea,
GenerateNewIdeas // < ---- add this type here -----
} from '@wasp/actions/types';
// ... other imports ...
import { generateIdeas } from './chain.js'; // < ---- this too -----
import { Rettiwt } from 'rettiwt-api'; // < ---- and this here -----

const twitter = Rettiwt({ // < ---- and this -----
kdt: process.env.KDT!,
twid: process.env.TWID!,
ct0: process.env.CT0!,
auth_token: process.env.AUTH_TOKEN!,
});

//... other stuff ...

export const generateNewIdeas: GenerateNewIdeas<unknown, void> = async (_args, context) => {
try {
// get the logged in user that Wasp passes to the action via the context
const user = context.user

if (!user) {
throw new HttpError(401, 'User is not authorized');
}

for (let h = 0; h < user.favUsers.length; h++) {
const favUser = user.favUsers[h];
const oneDayFromNow = new Date(Date.now() + 24 * 60 * 60 * 1000);
// convert oneDayFromNow to format YYYY-MM-DD
const endDate = oneDayFromNow.toISOString().split('T')[0];

// find the most recent tweet from the favUser
const mostRecentTweet = await context.entities.Tweet.findFirst({
where: {
authorUsername: favUser,
},
orderBy: {
tweetedAt: 'desc',
},
});

console.log('mostRecentTweet: ', mostRecentTweet)

const favUserTweets = await twitter.tweets.getTweets({
fromUsers: [favUser],
sinceId: mostRecentTweet?.tweetId || undefined, // get tweets since the most recent tweet if it exists
endDate: endDate, // endDate in format YYYY-MM-DD
});

const favUserTweetTexts = favUserTweets.list

for (let i = 0; i < favUserTweetTexts.length; i++) {
const tweet = favUserTweetTexts[i];

const existingTweet = await context.entities.User.findFirst({
where: {
id: user.id,
},
select: {
originalTweets: {
where: {
tweetId: tweet.id,
},
},
},
});

/**
* If the tweet already exists in the database, skip generating drafts and ideas for it.
*/
if (existingTweet) {
console.log('tweet already exists in db, skipping generating drafts...');
continue;
}

/**
* this is where the magic happens
*/
const draft = await generateIdeas(tweet.fullText, user.username);
console.log('draft: ', draft);

const originalTweet = await context.entities.Tweet.create({
data: {
tweetId: tweet.id,
content: tweet.fullText,
authorUsername: favUser,
tweetedAt: new Date(tweet.createdAt),
userId: user.id
},
});

let newTweetIdeas = draft.newTweetIdeas.split('\n');
newTweetIdeas = newTweetIdeas
.filter((idea) => idea.trim().length > 0)
.map((idea) => {
// remove all dashes that are not directly followed by a letter
idea = idea.replace(/-(?![a-zA-Z])/g, '');
idea = idea.replace(/"/g, '');
idea = idea.replace(/{/g, '');
idea = idea.replace(/}/g, '');
// remove hashtags and the words that follow them
idea = idea.replace(/#[a-zA-Z0-9]+/g, '');
idea = idea.replace(/^\s*[\r\n]/gm, ''); // remove new line breaks
idea = idea.trim();
// check if last character contains punctuation and if not add a period
if (idea.length > 1 && !idea[idea.length - 1].match(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g)) {
idea += '.';
}
return idea;
});
for (let j = 0; j < newTweetIdeas.length; j++) {
const newTweetIdea = newTweetIdeas[j];
const newIdea = await context.entities.GeneratedIdea.create({
data: {
content: newTweetIdea,
originalTweetId: originalTweet.id,
userId: user.id
},
});
console.log('newIdea saved to DB: ', newIdea);
}

const interestingTweetDraft = await context.entities.TweetDraft.create({
data: {
content: draft.interestingTweet,
originalTweetId: originalTweet.id,
notes: draft.notes,
userId: user.id
},
});

console.log('interestingTweetDraft saved to DB: ', interestingTweetDraft);

// create a delay to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));

}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

} catch (error: any) {
console.log('error', error);
throw new HttpError(500, error);
}
}

Ok! Nice work. There’s a lot going on above, so let’s just recap:

  • We loop through the array of our favorite users, as defined on our user entity in main.wasp,
  • Pull that user’s most recent tweets
  • Send that tweet to our generateIdeas function, which
    • searches our vector store for similar notes
    • asks GPT to generate similar, new ideas
    • sends those ideas in another prompt GPT to create a new, interesting tweet
    • returns the new ideas and interesting tweet
  • Create new GeneratedIdeas and a TweetDraft and saves them to our Postgres DB

Phew! We’re doing it 💪 

Fetching & Displaying Ideas

Defining a Server-side Query

Since we now have our chain of GPT prompts defined via LangChain and our server-side action, let’s go ahead and start implementing some front-end logic to fetch that data and display it to our users… which is basically only us at this point 🫂.

Just as we added a server-side action to generateNewIdeas we will now define a query to fetch those ideas.

Add the following query to your main.wasp file:

query getTweetDraftsWithIdeas {
fn: import { getTweetDraftsWithIdeas } from "@server/ideas.js",
entities: [TweetDraft]
}

In your src/server/ideas.ts file, below your generateNewIdeas action, add the query we just defined in our wasp file:

//... other imports ...
import type { GetTweetDraftsWithIdeas } from '@wasp/queries/types'; // <--- add this ---

// ... other functions ...

type TweetDraftsWithIdeas = {
id: number;
content: string;
notes: string;
createdAt: Date;
originalTweet: {
id: number;
content: string;
tweetId: string;
tweetedAt: Date;
ideas: GeneratedIdea[];
authorUsername: string;
};
}[];

export const getTweetDraftsWithIdeas: GetTweetDraftsWithIdeas<unknown, TweetDraftsWithIdeas> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const drafts = await context.entities.TweetDraft.findMany({
orderBy: {
originalTweet: {
tweetedAt: 'desc',
}
},
where: {
userId: context.user.id,
createdAt: {
gte: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // Get drafts created within the last 2 days
},
},
select: {
id: true,
content: true,
notes: true,
createdAt: true,
originalTweet: {
select: {
id: true,
tweetId: true,
content: true,
ideas: true,
tweetedAt: true,
authorUsername: true,
},
},
},
});

return drafts;
};

With this function we will be returning the tweet drafts we generate, along with our notes, the original tweet that inspired it, and the newly generated ideas.

Sweet!

Ok, but what good is a function that fetches the data if we’ve got nowhere to display it!?

Displaying Ideas Client-side

Let’s go now to our src/client/MainPage.tsx file (make sure it’s got the .tsx extension and not .jsx) and replace the contents with these below:

import waspLogo from './waspLogo.png'
import './Main.css'

const MainPage = () => {
return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
</div>
</div>
)
}
export default MainPage

At this point, you. might need to restart the wasp dev server running in your terminal to get the tailwind configuration to take effect (ctrl + c, then wasp start again).

You’ll now be prompted with the login / register screen. Go ahead and click on register and you will be automatically logged in and redirected to the main page, which at this point only has this:

Untitled

Let’s go back to our MainPage.tsx file and add the magic!

https://media3.giphy.com/media/ekv45izCuyXkXoHRaL/giphy.gif?cid=7941fdc6c3dszwj4xaoxg2kyj6xxdubjxn69m4qruhomhkut&ep=v1_gifs_search&rid=giphy.gif&ct=g

First, let’s create a buttons component so we don’t have to constantly style a new button. Create a new src/client/Button.tsx file:

import { ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}

export default function Button({ isLoading, children, ...otherProps }: ButtonProps) {
return (
<button
{...otherProps}
className={`flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 text-blue-500 font-bold px-3 py-1 text-sm rounded-lg ${isLoading ? ' pointer-events-none opacity-70' : 'cursor-pointer'}`}
>
{isLoading? 'Loading...' : children}
</button>
);
}

Now let’s add it to your AddNote.tsx component, replacing the original button with this one. The whole file should look like this:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';
import Button from './Button';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<Button isLoading={isIdeaEmbedding} onClick={handleEmbedIdea}>
Save Note
</Button>
</div>
);
}

Noice.

Next, we want our page to perform the following actions:

  1. create a button that runs our generateNewIdeas action when clicked
  2. define the query that fetches and caches the tweet drafts and ideas
  3. loop through the results and display them on the page

That’s exactly what the below code will do. Go ahead and replace the MainPage with it and take a minute to review what’s going on:

import waspLogo from './waspLogo.png';
import './Main.css';
import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import AddNote from './AddNote';
import Button from './Button';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
<AddNote />
<hr className='border border-t-1 border-neutral-100/70 w-full' />
<div className='flex flex-row justify-center w-1/4'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</div>
</div>
</div>
);
};
export default MainPage;

This is what you should see on the homepage now! 🎉

Untitled

But, if you clicked ‘generate new ideas’ and nothing happened, well that’s because we haven’t defined any favorite trend-setting twitter users to scrape tweets from. And there’s no way to do that from the UI at the moment, so let’s open up the database manager and add some manually.

In a new terminal tab, in the root of your project, run:

wasp db studio

Then, in a new browswer tab, at localhost:5555 you should see your database.

Go to user, and you should be the only user in there. Add the usernames of a couple of your favorite trend-setting twitter users.

Untitled

Make sure the accounts have tweeted recently or your function won’t be able to scrape or generate anything!

Hey ✋

While you’re at it, if you’re liking this tutorial, give me a follow @hot_town for more future content like this

After adding the twitter usernames, make sure you click save 1 change.

Go back to your client and click the Generate New Ideas button again. This might take a while depending on how many tweets it’s generating ideas for, so be patient — and watch the console output in your terminal if you’re curious ;)

Untitled

Awesome! Now we should be getting back some generated ideas from our twitter “intern” which will help us brainstorm further notes and generate our own BANGER TWEETS.

But it would be cool to also display the tweet these ideas are referencing from the beginning. That way we’d have a bit more context on where the ideas came from.

Let’s do that then! In your MainPage file, at the very top, add the following import:

import { TwitterTweetEmbed } from 'react-twitter-embed';

This allows us to embed tweets with that nice twitter styling.

We already added this dependency to our main.wasp file at the beginning of the tutorial, so we can just import and start embedding tweets.

Let’s try it out now in our MainPage by adding the following snippet above our <h2>Tweet Draft</h2> element:

//...

<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>

<h2>Tweet Draft</h2>
//...

Great. Now we should be sitting pretty 😻

Untitled

You might remember from the beginning of the tutorial when we defined the LLM calls, that if your vector store notes don’t turn back a cosine similarity of at least 0.7, your agent will generate its own ideas entirely without using your notes as a guide.

And since we have NO notes in our vector store at the moment, that’s exactly what it is doing. Which is fine, because we can let it brainstorm for us, and we can select our favorite notes and edit and add them as we see fit.

So you can go ahead and start adding notes whenever you feel like it 📝.

But, we’ve added our favorite twitter users to the database manually. It would be preferable to do it via an account settings page, right? Let’s make one then.

Creating an Account Settings Page

First, add the route and page to your main.wasp config file, under the other routes:

//...

route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
authRequired: true,
component: import Account from "@client/AccountPage"
}

Next, let’s create a new page, src/client/AccountPage.tsx:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
{JSON.stringify(user, null, 2)}
</div>
</div>
);
};

export default AccountPage;

When you navigate to localhost:3000/account, you’ll notice two things, one of them being a logout button. You can see in our SettingsPage above that we imported a Wasp-provided logout function. We get this “for free” since we defined our auth strategy in the main.wasp file — a big time-saver!

Untitled

Because we also defined the AccountPage route with the authRequired: true property, Wasp will automatically pass the logged in user as a prop argument to our page. We can use the user object to display and update our favUsers, just as we can see in the image above.

To do that, let’s define a new updateAccount action in our main.wasp file:

action updateAccount {
fn: import { updateAccount } from "@server/account.js",
entities: [User]
}

Next, let’s create the updateAccount action in a new file, src/server/account.ts:

import type { UpdateAccount } from "@wasp/actions/types";
import HttpError from "@wasp/core/HttpError.js";

export const updateAccount: UpdateAccount<{ favUsers: string[] }, void> = async ({ favUsers }, context) => {
if (!context.user) {
throw new HttpError(401, "User is not authorized");
}

try {
await context.entities.User.update({
where: { id: context.user.id },
data: { favUsers },
});

} catch (error: any) {
throw new HttpError(500, error.message);
}
}

Right. Now it’s time to put it all together in our Account page. We’re going to create a form for adding new twitter users to scrape tweets from, so at the bottom of your src/client/AccountPage.tsx, below your other code, add the following component:

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
//...
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

This component takes care of adding the logged in user’s favUsers array to state, and displaying that in information in a set of input components.

The only thing missing from it is to add our updateAccount action we just defined earlier. So at the top of the file, let’s import it and add the logic to our InputFields submit handler

import updateAccount from '@wasp/actions/updateAccount'; // <--- add this import

//...

const handleSubmit = async () => { // < --- add this function
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

Also, in your AccountPage make sure to replace the line {JSON.stringify(user, null, 2)} with the newly created component <InputFields user={user} />.

Here is what the entire AccountPage.tsx file should now look like in case you get stuck:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';
import updateAccount from '@wasp/actions/updateAccount'

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
<InputFields user={user} />
</div>
</div>
);
};

export default AccountPage;

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

And here’s what your AccountPage should look like when navigating to localhost:3000/account (note: the styling may be a bit ugly, but we’ll take care of that later):

Untitled

Fantastic. So we’ve got the majority of the app logic finished — our own personal twitter “intern” to help us all become thought leaders and thread bois 🤣.

Adding a Cron Job

But wouldn’t it be cool if we could automate the Generate New Ideas process? Each time you click the button, it takes quite a while for tweets to be scraped, and ideas to be generated, especially if we are generating ideas for a lot of new tweets.

So it would be nicer if we had a cron job (recurring task), that ran automatically in the background at a set interval.

With Wasp, that’s also super easy to set up. To do so, let’s go to our main.wasp file and add our job at the very bottom:

//...

job newIdeasJob {
executor: PgBoss,
perform: {
fn: import generateNewIdeasWorker from "@server/worker/generateNewIdeasWorker.js"
},
entities: [User, GeneratedIdea, Tweet, TweetDraft],
schedule: {
// run cron job every 30 minutes
cron: "*/30 * * * *",
executorOptions: {
pgBoss: {=json { "retryLimit": 2 } json=},
}
}
}

Let’s run through the code above:

  • Jobs use pg-boss, a postgres extension, to queue and run tasks under the hood.
  • with perform we’re telling the job what function we want it to call: generateNewIdeasWorker
  • just like actions and queries, we have to tell the job which entities we want to give it access to. In this case, we will need access to all of our entities.
  • the schedule allows us to pass some options to pg-boss so that we can make it a recurring task. In this case, I set it to run every 30 minutes, but you can set it to any interval you’d like (tip: change the comment and let github co-pilot write the cron for you). We also tell pg-boss to retry a failed job two times.

Perfect. So now, our app will automatically scrape our favorite users’ tweets and generate new ideas for us every 30 minutes. This way, if we revisit the app after a few days, all the content will already be there and we won’t have to wait a long time for it to generate it for us. We also make sure we never miss out on generating ideas for older tweets.

But for that to happen, we have to define the function our job will call. To do this, create a new directory worker within the server folder, and within it a new file: src/server/worker/generateNewIdeasWorker

import { generateNewIdeas } from '../ideas.js';

export default async function generateNewIdeasWorker(_args: unknown, context: any) {
try {
console.log('Running recurring task: generateNewIdeasWorker')
const allUsers = await context.entities.User.findMany({});

for (const user of allUsers) {
context.user = user;
console.log('Generating new ideas for user: ', user.username);
await generateNewIdeas(undefined as never, context);
console.log('Done generating new ideas for user: ', user.username)
}

} catch (error: any) {
console.log('Recurring task error: ', error);
}
}

In this file, all we’re doing is looping through all the users in our database, and passing them via the context object to our generateNewIdeas action. The nice thing about jobs is that Wasp automatically passes the context object to these functions, which we can then pass along to our action.

So now, at the interval that you set (e.g. 30 minutes), you should notice the logs being printed to the console whenever your job starts automatically running.

[Server]  Generating new ideas for user:  vinny

Alright, things are looking pretty good now, but let’s not forget to add a page to view all the notes we added and embedded to our vector store!

Adding a Notes Page

Go ahead and add the following route to your main.wasp file:

route NotesPage { path: "/notes", to: NotesPage }
page NotesPage {
authRequired: true,
component: import Notes from "@client/NotesPage"
}

Create the complementary page, src/client/NotesPage.tsx and add the following boilerplate just to get started (we’ll add the rest later):

const NotesPage = () => {

return (
<>Notes</>
);
};

export default NotesPage;

It would be nice if we had a simple Nav Bar to navigate back and forth between our two pages. It would also be cool if we had our <AddNote /> input component on all pages, that way it’s easy for us to add an idea whenever inspiration strikes.

Rather than copying the NavBar and AddNote code to both pages, let’s create a wrapper, or “root”, component for our entire app so that all of our pages have the same Nav Bar and layout.

To do that, in our main.wasp file, let’s define our root component by adding a client property to our app configuration at the very top of the file. This is how the entire app object should look like now:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
client: {
rootComponent: import App from "@client/App",
},
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// entities, operations, routes, and other stuff...

Next, create a new file src/client/App.tsx with the following content:

import './Main.css';
import AddNote from './AddNote';
import { ReactNode } from 'react';
import useAuth from '@wasp/auth/useAuth';

const App = ({ children }: { children: ReactNode }) => {

const { data: user } = useAuth();

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<div className='flex flex-row justify-between items-center w-1/2 mb-6 text-neutral-600 px-2'>
<div className='flex justify-start w-1/3'>
<a href='/' className='hover:underline cursor-pointer'>
🤖 Generated Ideas
</a>
</div>
<div className='flex justify-center w-1/3'>
<a href='/notes' className='hover:underline cursor-pointer'>
📝 My Notes
</a>
</div>
<div className='flex justify-end w-1/3'>
<a href='/account' className='hover:underline cursor-pointer'>
👤 Account
</a>
</div>
</div>

<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
{!!user && <AddNote />}
<hr className='border border-t-1 border-neutral-100/70 w-full' />
{children}
</div>
</div>
</div>
);
};

export default App;

With this defined, Wasp will know to pass all other routes as children through our App component. That way, we will always show the Nav Bar and AddNote component on the top of every page.

We also take advantage of Wasp’s handy useAuth hook to check if a user is logged in, and if so we show the AddNote component.

Now, we can delete the duplicate code on our MainPage. This is what it should look like now:

import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import Button from './Button';
import { TwitterTweetEmbed } from 'react-twitter-embed';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<>
<div className='flex flex-row justify-center w-full'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</>
);
};
export default MainPage;

Next, we need to create a query that allows us to fetch all of our added notes and ideas that have been embedded in our vector store.

For that, we need to define a new query in our main.wasp file:

query getEmbeddedNotes {
fn: import { getEmbeddedNotes } from "@server/ideas.js",
entities: [GeneratedIdea]
}

We then need to create that query at the bottom of our src/actions/ideas.ts file:

// first import the type at the top of the file
import type { GetEmbeddedNotes, GetTweetDraftsWithIdeas } from '@wasp/queries/types';

//...

export const getEmbeddedNotes: GetEmbeddedNotes<never, GeneratedIdea[]> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const notes = await context.entities.GeneratedIdea.findMany({
where: {
userId: context.user.id,
isEmbedded: true,
},
orderBy: {
createdAt: 'desc',
},
});

return notes;
}

Now let’s go back to our src/client/NotesPage.tsx and add our query. Our new file will look like this:

import { useQuery } from '@wasp/queries';
import getEmbeddedNotes from '@wasp/queries/getEmbeddedNotes';

const NotesPage = () => {
const { data: notes, isLoading, error } = useQuery(getEmbeddedNotes);

if (isLoading) <div>Loading...</div>;
if (error) <div>Error: {error.message}</div>;

return (
<>
<h2 className='text-2xl font-bold'>My Notes</h2>
{notes && notes.length > 0 ? (
notes.map((note) => (
<div key={note.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{note.content}</div>
</div>
</div>
))
) : notes && notes.length === 0 && (
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>No notes yet</div>
</div>
)}
</>
);
};

export default NotesPage;

Cool! Now we should be fetching all our embedded notes and ideas, signified by the isEmbedded tag in our postgres database. Your Notes page should now look something like this:

Untitled

You Did it! Your own Twitter Intern 🤖

Help me help you

🌟 If you haven’t yet, please star us on GitHub, especially if you found this useful! If you do, it helps support us in creating more content like this. And if you don’t… well, we will deal with it, I guess.

https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif

And that’s it! You’ve now got yourself a semi-autonomous twitter brainstorming agent to help inspire new ideas and keep you actively contributing 🚀

There’s way more you can do with these tools, but this is a great start.

Remember, if you want to see a more advanced version of this app which utilizes the official Twitter API to send tweets, gives you the ability to edit and add generated notes on the fly, has manual similarity search for all your notes, and more, then you can check out the 💥 Banger Tweet Bot 🤖.

And, once again, here's the repo for the finished app we built in this tutorial: Personal Twitter Intern

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

· 46 min read
Vinny

TL;DR

LangChain, ChatGPT, and other emerging technology have made it possible to build some really creative tools.

In this tutorial, we’ll build a full-stack web app that acts as our own personal Twitter Agent, or “intern”, as I like to call it. It keeps track of your notes and ideas, and uses them — along with tweets from trending-setting twitter users — to brainstorm new ideas and write tweet drafts for you! 💥

BTW, If you get stuck during the tutorial, or at any point just want to check out the full, final repo of the app we're building, here it is: https://github.com/vincanger/twitter-intern

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built in compiler that lets you build your app in a day and deploy with a single CLI command.

We’re working hard to help you build performant web apps as easily as possibly — including making these tutorials, which are released weekly!

We would be super grateful if you could help us out by starring our repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

https://media2.giphy.com/media/d0Pkp9OMIBdC0/giphy.gif?cid=7941fdc6b39mgj7h8orvi0f4bjebceyx4gj0ih1xb6s05ujc&ep=v1_gifs_search&rid=giphy.gif&ct=g

…even Ron would star Wasp on GitHub 🤩

Background

Twitter is a great marketing tool. It’s also a great way to explore ideas and refine your own. But it can be time-consuming and difficult to maintain a tweeting habit.

https://media0.giphy.com/media/WSrR5xkvljaFMe7UPo/giphy.gif?cid=7941fdc6g9o3drj567dbwyuo1c66x76eq8awc2r1oop8oypl&ep=v1_gifs_search&rid=giphy.gif&ct=g

That’s why I decided to build my own personal twitter agent with LangChain on the basis of these assumptions:

🧠 LLMs (like ChatGPT) aren’t the best writers, but they ARE great at brainstorming new ideas.

📊 Certain twitter users drive the majority of discourse within certain niches, i.e. trend-setters influence what’s being discussed at the moment.

💡 the Agent needs context in order to generate ideas relevant to YOU and your opinions, so it should have access to your notes, ideas, tweets, etc.

So instead of trying to build a fully autonomous agent that does the tweeting for you, I thought it would be better to build an agent that does the BRAINSTORMING for you, based on your favorite trend-setting twitter users as well as your own ideas.

Imagine it like an intern that does the grunt work, while you do the curating!

https://media.giphy.com/media/26DNdV3b6dqn1jzR6/giphy.gif

In order to accomplish this, we need to take advantage of a few hot AI tools:

  • Embeddings and Vector Databases
  • LLMs (Large Language Models), such as ChatGPT
  • LangChain and sequential “chains” of LLM calls

Embeddings and Vector Databases give us a powerful way to perform similarity searches on our own notes and ideas.

If you’re not familiar with similarity search, the simplest way to describe what similarity search is by comparing it to a normal google search. In a normal search, the phrase “a mouse eats cheese” will return results with a combination of those words only. But a vector-based similarity search, on the other hand, would return those words, as well as results with related words such as “dog”, “cat”, “bone”, and “fish”.

You can see why that’s so powerful, because if we have non-exact but related notes, our similarity search will still return them!

https://media2.giphy.com/media/xUySTD7evBn33BMq3K/giphy.gif?cid=7941fdc6273if8qfk83gbnv8uabc4occ0tnyzk0g0gfh0qg5&ep=v1_gifs_search&rid=giphy.gif&ct=g

For example, if our favorite trend-setting twitter user makes a post about the benefits of typescript, but we only have a note on “our favorite React hooks”, our similarity search would still likely return such a result. And that’s huge!

Once we get those notes, we can pass them to the ChatGPT completion API along with a prompt to generate more ideas. The result from this prompt will then be sent to another prompt with instructions to generate a draft tweet. We save these sweet results to our Postgres relational database.

This “chain” of prompting is essentially where the LangChain package gets its name 🙂

The flow of information through the app

This approach will give us a wealth of new ideas and tweet drafts related to our favorite trend-setting twitter users’ tweets. We can look through these, edit and save our favorite ideas to our “notes” vector store, or maybe send off some tweets.

I’ve personally been using this app for a while now, and not only has it generated some great ideas, but it also helps to inspire new ones (even if some of the ideas it generates are “meh”), which is why I included an “Add Note” feature front and center to the nav bar

twitter-agent-add-note.png

Ok. Enough background. Let’s start building your own personal twitter intern! 🤖

BTW, if you get stuck at all while following the tutorial, you can always reference this tutorial’s repo, which has the finished app: Twitter Intern GitHub Repo

Configuration

Set up your Wasp project

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

# First, install Wasp by running this in your terminal:

curl -sSL https://get.wasp-lang.dev/installer.sh | sh

# next, create a new project:

wasp new twitter-agent

# cd into the new directory and start the project:

cd twitter-agent && wasp start

Great! When running wasp start, Wasp will install all the necessary npm packages, start our server on port 3001, and our React client on port 3000. Head to localhost:3000 in your browser to check it out.

Untitled

Tip ℹ️

you can install the Wasp vscode extension for the best developer experience.

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s start adding some server-side code.

Server-Side & Database Entities

Start by adding a .env.server file in the root directory of your project:

# https://platform.openai.com/account/api-keys
OPENAI_API_KEY=

# sign up for a free tier account at https://www.pinecone.io/
PINECONE_API_KEY=
# will be a location, e.g 'us-west4-gcp-free'
PINECONE_ENV=

# We will fill these in later during the Twitter Scraping section
# Twitter details -- only needed once for Rettiwt.account.login() to get the tokens
TWITTER_EMAIL=
TWITTER_HANDLE=
TWITTER_PASSWORD=

# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT=
TWID=
CT0=
AUTH_TOKEN=

We need a way for us to store all our great ideas. So let’s first head to Pinecone.io and set up a free trial account.

Untitled

In the Pinecone dashboard, go to API keys and create a new one. Copy and paste your Environment and API Key into .env.server

Do the same for OpenAI, by creating an account and key at https://platform.openai.com/account/api-keys

Now let’s replace the contents of the main.wasp config file, which is like the “skeleton” of your app, with the code below. This will configure most of the fullstack app for you 🤯

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
head: [
"<script async src='https://platform.twitter.com/widgets.js' charset='utf-8'></script>"
],
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// ### Database Models

entity Tweet {=psl
id Int @id @default(autoincrement())
tweetId String
authorUsername String
content String
tweetedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
drafts TweetDraft[]
ideas GeneratedIdea[]
psl=}

entity TweetDraft {=psl
id Int @id @default(autoincrement())
content String
notes String
originalTweet Tweet @relation(fields: [originalTweetId], references: [id])
originalTweetId Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

entity GeneratedIdea {=psl
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
originalTweet Tweet? @relation(fields: [originalTweetId], references: [id])
originalTweetId Int?
isEmbedded Boolean @default(false)
psl=}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
createdAt DateTime @default(now())
favUsers String[]
originalTweets Tweet[]
tweetDrafts TweetDraft[]
generatedIdeas GeneratedIdea[]
psl=}

// <<< Client Pages & Routes

route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true,
component: import Main from "@client/MainPage"
}

//...
note

You might have noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)

With this, our app structure is mostly defined and Wasp will take care of a ton of configuration for us.

Database Setup

But we still need to get a postgres database running. Usually this can be pretty annoying, but with Wasp, just have Docker Deskop installed and running, then open up another separate terminal tab/window and then run:

wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just leave this terminal tab, along with docker desktop, open and running in the background.

In a different terminal tab, run:

wasp db migrate-dev

and make sure to give your database migration a name.

If you stopped the wasp dev server to run this command, go ahead and start it again with wasp start.

At this point, our app will be navigating us to localhost:3000/login but because we haven’t implemented a login screen/flow yet, we will be seeing a blank screen. Don’t worry, we’ll get to that.

Embedding Ideas & Notes

Server Action

First though, in the main.wasp config file, let’s define a server action for saving notes and ideas. Go ahead and add the code below to the bottom of the file:

// main.wasp

//...
// <<< Client Pages & Routes

route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true,
component: import Main from "@client/MainPage"
}

// !!! Actions

action embedIdea {
fn: import { embedIdea } from "@server/ideas.js",
entities: [GeneratedIdea]
}

With the action declared, let’s create it. Make a new file, .src/server/ideas.ts in and add the following code:

import type { EmbedIdea } from '@wasp/actions/types';
import type { GeneratedIdea } from '@wasp/entities';
import HttpError from '@wasp/core/HttpError.js';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

export const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

/**
* Embeds a single idea into the vector store
*/
export const embedIdea: EmbedIdea<{ idea: string }, GeneratedIdea> = async ({ idea }, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

console.log('idea: ', idea);

try {
let newIdea = await context.entities.GeneratedIdea.create({
data: {
content: idea,
userId: context.user.id,
},
});


if (!newIdea) {
throw new HttpError(404, 'Idea not found');
}

const pinecone = await initPinecone();

// we need to create an index to save the vector embeddings to
// an index is similar to a table in relational database world
const availableIndexes = await pinecone.listIndexes();
if (!availableIndexes.includes('embeds-test')) {
console.log('creating index');
await pinecone.createIndex({
createRequest: {
name: 'embeds-test',
// open ai uses 1536 dimensions for their embeddings
dimension: 1536,
},
});
}

const pineconeIndex = pinecone.Index('embeds-test');

// the LangChain vectorStore wrapper
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: context.user.username,
});

// create a document with the idea's content to be embedded
const ideaDoc = new Document({
metadata: { type: 'note' },
pageContent: newIdea.content,
});

// add the document to the vectore store along with its id
await vectorStore.addDocuments([ideaDoc], [newIdea.id.toString()]);

newIdea = await context.entities.GeneratedIdea.update({
where: {
id: newIdea.id,
},
data: {
isEmbedded: true,
},
});
console.log('idea embedded successfully!', newIdea);
return newIdea;
} catch (error: any) {
throw new Error(error);
}
};
info

We’ve defined the action function in our main.wasp file as coming from ‘@server/ideas.js’ but we’re creating an ideas.ts file. What's up with that?!

Well, Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). It does not apply to client files.

Great! Now we have a server action for adding notes and ideas to our vector database. And we didn’t even have to configure a server ourselves (thanks, Wasp 🙂).

Let's take a step back and walk through the code we just wrote though:

  1. We create a new Pinecone client and initialize it with our API key and environment.
  2. We create a new OpenAIEmbeddings client and initialize it with our OpenAI API key.
  3. We create a new index in our Pinecone database to store our vector embeddings.
  4. We create a new PineconeStore, which is a LangChain wrapper around our Pinecone client and our OpenAIEmbeddings client.
  5. We create a new Document with the idea’s content to be embedded.
  6. We add the document to the vector store along with its id.
  7. We also update the idea in our Postgres database to mark it as embedded.

Now we want to create the client-side functionality for adding ideas, but you’ll remember we defined an auth object in our wasp config file. So we’ll need to add the ability to log in before we do anything on the frontend.

Authentication

Let’s add that quickly by adding a new a Route and Page definition to our main.wasp file

//...

route LoginPageRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/LoginPage"
}

…and create the file src/client/LoginPage.tsx with the following content:

import { LoginForm } from '@wasp/auth/forms/Login';
import { SignupForm } from '@wasp/auth/forms/Signup';
import { useState } from 'react';

export default () => {
const [showSignupForm, setShowSignupForm] = useState(false);

const handleShowSignupForm = () => {
setShowSignupForm((x) => !x);
};

return (
<>
{showSignupForm ? <SignupForm /> : <LoginForm />}
<div onClick={handleShowSignupForm} className='underline cursor-pointer hover:opacity-80'>
{showSignupForm ? 'Already Registered? Login!' : 'No Account? Sign up!'}
</div>
</>
);
};
info

In the auth object on the main.wasp file, we used the usernameAndPassword method which is the simplest form of auth Wasp offers. If you’re interested, Wasp does provide abstractions for Google, Github, and Email Verified Authentication, but we will stick with the simplest auth for this tutorial.

With authentication all set up, if we try to go to localhost:3000 we will be automatically directed to the login/register form.

You’ll see that Wasp creates Login and Signup forms for us because of the auth object we defined in the main.wasp file. Sweet! 🎉

But even though we’ve added some style classes, we haven’t set up any css styling so it will probably be pretty ugly right about now.

🤢 Barf.

Untitled

Adding Tailwind CSS

Luckily, Wasp comes with tailwind css support, so all we have to do to get that working is add the following files in the root directory of the project:

.
├── main.wasp
├── src
│ ├── client
│ ├── server
│ └── shared
├── postcss.config.cjs # add this file here
├── tailwind.config.cjs # and this here too
└── .wasproot

postcss.config.cjs

module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

Finally, replace the contents of your src/client/Main.css file with these lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now we’ve got the magic of tailwind css on our sides! 🎨 We’ll get to styling later though. Patience, young grasshopper.

Adding Notes Client-side

From here, let’s create the complimentary client-side components for adding notes to the vector store. Create a new .src/client/AddNote.tsx file with the following contents:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<button
onClick={handleEmbedIdea}
className='flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 font-bold px-3 py-1 text-sm text-blue-500 whitespace-nowrap rounded-lg'
>
{isIdeaEmbedding ? 'Loading...' : 'Save Note'}
</button>
</div>
);
}

Here we’re using the embedIdea action we defined earlier to add our idea to the vector store. We’re also using the useState hook to keep track of the idea we’re adding, as well as the loading state of the button.

So now we have a way to add our own ideas and notes to our vector store. Pretty sweet!

Generating New Ideas & Tweet Drafts

Using LangChain's Sequential Chains

Now we need to set up the sequential chain of LLM calls that LangChain is so great at.

Here are the steps we will take:

  1. define a function that uses LangChain to initiate a “chain” of API calls to OpenAI’s ChatGPT completions endpoint.
    1. this function takes a tweet that we pulled from one of our favorite twitter users as an argument, searches our vector store for similar notes & ideas, and returns a list of new “brainstormed” based on the example tweet and our notes.
  2. define a new action that loops through our favorite users array, pulls their most recent tweets, and sends them to our LangChain function mentioned above

So let’s start again by creating our LangChain function. Make a new src/server/chain.ts file:

import { ChatOpenAI } from 'langchain/chat_models/openai';
import { LLMChain, SequentialChain } from 'langchain/chains';
import { PromptTemplate } from 'langchain/prompts';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

export const generateIdeas = async (exampleTweet: string, username: string) => {
try {
// remove quotes and curly braces as not to confuse langchain template parser
exampleTweet = exampleTweet.replace(/"/g, '');
exampleTweet = exampleTweet.replace(/{/g, '');
exampleTweet = exampleTweet.replace(/}/g, '');

const pinecone = await initPinecone();

console.log('list indexes', await pinecone.listIndexes());

// find the index we created earlier
const pineconeIndex = pinecone.Index('embeds-test');

const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: username,
});

//
// sequential tweet chain begin --- >
//
/**
* vector store results for notes similar to the original tweet
*/
const searchRes = await vectorStore.similaritySearchWithScore(exampleTweet, 2);
console.log('searchRes: ', searchRes);
let notes = searchRes
.filter((res) => res[1] > 0.7) // filter out strings that have less than %70 similarity
.map((res) => res[0].pageContent)
.join(' ');

console.log('\n\n similarity search results of our notes-> ', notes);

if (!notes || notes.length <= 2) {
notes = exampleTweet;
}

const tweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 0.8, // 0 - 2 with 0 being more deterministic and 2 being most "loose". Past 1.3 the results tend to be more incoherent.
modelName: 'gpt-3.5-turbo',
});

const tweetTemplate = `You are an expert idea generator. You will be given a user's notes and your goal is to use this information to brainstorm other novel ideas.

Notes: {notes}

Ideas Brainstorm:
-`;

const tweetPromptTemplate = new PromptTemplate({
template: tweetTemplate,
inputVariables: ['notes'],
});

const tweetChain = new LLMChain({
llm: tweetLlm,
prompt: tweetPromptTemplate,
outputKey: 'newTweetIdeas',
});

const interestingTweetTemplate = `You are an expert interesting tweet generator. You will be given some tweet ideas and your goal is to choose one, and write a tweet based on it. Structure the tweet in an informal yet serious tone and do NOT include hashtags in the tweet!

Tweet Ideas: {newTweetIdeas}

Interesting Tweet:`;

const interestingTweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 1.1,
modelName: 'gpt-3.5-turbo',
});

const interestingTweetPrompt = new PromptTemplate({
template: interestingTweetTemplate,
inputVariables: ['newTweetIdeas'],
});

const interestingTweetChain = new LLMChain({
llm: interestingTweetLlm,
prompt: interestingTweetPrompt,
outputKey: 'interestingTweet',
});

const overallChain = new SequentialChain({
chains: [tweetChain, interestingTweetChain],
inputVariables: ['notes'],
outputVariables: ['newTweetIdeas', 'interestingTweet'],
verbose: false,
});

type ChainDraftResponse = {
newTweetIdeas: string;
interestingTweet: string;
notes: string;
};

const res1 = (await overallChain.call({
notes,
})) as ChainDraftResponse;

return {
...res1,
notes,
};
} catch (error: any) {
throw new Error(error);
}
};

Great! Let's run through the above code real quick:

  1. Initialize the Pinecone client
  2. Find our pinecone index (i.e. table) that we created earlier and initialize a new PineconeStore with LangChain
  3. Search our vector store for notes similar to the example tweet, filtering out any results that have less than %70 similarity
  4. Create a new ChatGPT completion chain that takes our notes as input and generates new tweet ideas
  5. Create a new ChatGPT completion chain that takes the new tweet ideas as input and generates a new tweet draft
  6. Create a new SequentialChain and combine the above two chains together so that we can pass it our notes as input and it returns the new tweet ideas and the new tweet draft as output
VECTOR COSINE SIMILARITY SCORES

A good similarity threshold for cosine similarity search on text strings depends on the specific application and the desired level of strictness in matching. Cosine similarity scores range between 0 and 1, with 0 meaning no similarity and 1 meaning completely identical text strings.

  • 0.8-0.9 = strict
  • 0.6-0.8 = moderate
  • 0.5 = relaxed.

In our case, we went for a moderate similarity threshold of 0.7, which means that we will only return notes that are at least 70% similar to the example tweet.

With this function, we will get our newTweetIdeas and our interestingTweet draft back as results that we can use within our server-side action.

Scraping Twitter

Before we can pass an exampleTweet as an argument to our newly created Sequential Chain, we need to fetch it first!

To do this, we're going to use the Rettiwt-Api (which is just Twitter written backwards). Because it's an unofficial API there are a few caveats:

  1. We have to use the rettiwt client to login to our twitter account once. We will output the tokens it returns via a script and save those in our .env.server file for later.
  2. It's best to use an alternative account for this process. If you don't have an alternative account, go ahead and register a new one now.
⚠️

The use of an unofficial Twitter client, Rettiwt, is for illustrative purposes only. It's crucial that you familiarize yourself with Twitter's policies and rules regarding scraping before implementing these methods. Any abuse or misuse of these scripts and techniques may lead to actions taken against your Twitter account. We hold no responsibility for any consequences arising from your personal use of this tutorial and/or the related scripts. It is intended purely for learning and educational purposes.

Let's go ahead and create a new folder in src/server called scripts with a file inside called tokens.ts. This will be our script that we will run only once, just so that we get the necessary tokens to pass to our Rettiwt client.

We want to avoid running this script many times otherwise our account could get rate-limited. This shouldn't be an issue though, because once we return the tokens, they are valid for up to a year.

So inside src/server/scripts/tokens.ts add the following code:

import { Rettiwt } from 'rettiwt-api'; 

/**
* This is a script we can now run from the cli with `wasp db seed`
* IMPORTANT! We only want to run this script once, after which we save the tokens
* in the .env.server file. They should be good for up to a year.
*/
export const getTwitterTokens = async () => {
const tokens = await Rettiwt().account.login(
process.env.TWITTER_EMAIL!,
process.env.TWITTER_HANDLE!,
process.env.TWITTER_PASSWORD!
);

console.log('tokens: ', tokens)
};

Make sure to add your twitter login details to our .env.server file, if you haven't already!

Great. To be able to run this script via a simple Wasp CLI command, add it via the seeds array within the db object at the top of your main.wasp file:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
//...
db: {
system: PostgreSQL,
seeds: [ // <---------- add this
import { getTwitterTokens } from "@server/scripts/tokens.js",
]
},
//...

Nice! Now for the fun part :)

in your terminal, at the root of your project, run wasp db seed, and you should see the tokens output to the terminal similar to this:

[Db]      Running seed: getTwitterTokens
[Db] tokens: { // your tokens... }

Copy and paste those tokens into your .env.server file:


# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT='...'
TWID='...'
CT0='...'
AUTH_TOKEN='...'

Now with that, we should be able to access our favorite trend-setting users' recent tweets and use them to help us brainstorm new ideas!

Server Action

Ok, so we've got the tokens we need to get our trend-setting example tweets, and we've got a function that runs our similarity search and sequential chain of LLM calls.

Now let’s define an action in our main.wasp file that pulls it all together:

// actions...

action generateNewIdeas {
fn: import { generateNewIdeas } from "@server/ideas.js",
entities: [GeneratedIdea, Tweet, TweetDraft, User]
}

…and then create that action within src/server/ideas.ts


import type {
EmbedIdea,
GenerateNewIdeas // < ---- add this type here -----
} from '@wasp/actions/types';
// ... other imports ...
import { generateIdeas } from './chain.js'; // < ---- this too -----
import { Rettiwt } from 'rettiwt-api'; // < ---- and this here -----

const twitter = Rettiwt({ // < ---- and this -----
kdt: process.env.KDT!,
twid: process.env.TWID!,
ct0: process.env.CT0!,
auth_token: process.env.AUTH_TOKEN!,
});

//... other stuff ...

export const generateNewIdeas: GenerateNewIdeas<unknown, void> = async (_args, context) => {
try {
// get the logged in user that Wasp passes to the action via the context
const user = context.user

if (!user) {
throw new HttpError(401, 'User is not authorized');
}

for (let h = 0; h < user.favUsers.length; h++) {
const favUser = user.favUsers[h];
const oneDayFromNow = new Date(Date.now() + 24 * 60 * 60 * 1000);
// convert oneDayFromNow to format YYYY-MM-DD
const endDate = oneDayFromNow.toISOString().split('T')[0];

// find the most recent tweet from the favUser
const mostRecentTweet = await context.entities.Tweet.findFirst({
where: {
authorUsername: favUser,
},
orderBy: {
tweetedAt: 'desc',
},
});

console.log('mostRecentTweet: ', mostRecentTweet)

const favUserTweets = await twitter.tweets.getTweets({
fromUsers: [favUser],
sinceId: mostRecentTweet?.tweetId || undefined, // get tweets since the most recent tweet if it exists
endDate: endDate, // endDate in format YYYY-MM-DD
});

const favUserTweetTexts = favUserTweets.list

for (let i = 0; i < favUserTweetTexts.length; i++) {
const tweet = favUserTweetTexts[i];

const existingTweet = await context.entities.User.findFirst({
where: {
id: user.id,
},
select: {
originalTweets: {
where: {
tweetId: tweet.id,
},
},
},
});

/**
* If the tweet already exists in the database, skip generating drafts and ideas for it.
*/
if (existingTweet) {
console.log('tweet already exists in db, skipping generating drafts...');
continue;
}

/**
* this is where the magic happens
*/
const draft = await generateIdeas(tweet.fullText, user.username);
console.log('draft: ', draft);

const originalTweet = await context.entities.Tweet.create({
data: {
tweetId: tweet.id,
content: tweet.fullText,
authorUsername: favUser,
tweetedAt: new Date(tweet.createdAt),
userId: user.id
},
});

let newTweetIdeas = draft.newTweetIdeas.split('\n');
newTweetIdeas = newTweetIdeas
.filter((idea) => idea.trim().length > 0)
.map((idea) => {
// remove all dashes that are not directly followed by a letter
idea = idea.replace(/-(?![a-zA-Z])/g, '');
idea = idea.replace(/"/g, '');
idea = idea.replace(/{/g, '');
idea = idea.replace(/}/g, '');
// remove hashtags and the words that follow them
idea = idea.replace(/#[a-zA-Z0-9]+/g, '');
idea = idea.replace(/^\s*[\r\n]/gm, ''); // remove new line breaks
idea = idea.trim();
// check if last character contains punctuation and if not add a period
if (idea.length > 1 && !idea[idea.length - 1].match(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g)) {
idea += '.';
}
return idea;
});
for (let j = 0; j < newTweetIdeas.length; j++) {
const newTweetIdea = newTweetIdeas[j];
const newIdea = await context.entities.GeneratedIdea.create({
data: {
content: newTweetIdea,
originalTweetId: originalTweet.id,
userId: user.id
},
});
console.log('newIdea saved to DB: ', newIdea);
}

const interestingTweetDraft = await context.entities.TweetDraft.create({
data: {
content: draft.interestingTweet,
originalTweetId: originalTweet.id,
notes: draft.notes,
userId: user.id
},
});

console.log('interestingTweetDraft saved to DB: ', interestingTweetDraft);

// create a delay to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));

}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

} catch (error: any) {
console.log('error', error);
throw new HttpError(500, error);
}
}

Ok! Nice work. There’s a lot going on above, so let’s just recap:

  • We loop through the array of our favorite users, as defined on our user entity in main.wasp,
  • Pull that user’s most recent tweets
  • Send that tweet to our generateIdeas function, which
    • searches our vector store for similar notes
    • asks GPT to generate similar, new ideas
    • sends those ideas in another prompt GPT to create a new, interesting tweet
    • returns the new ideas and interesting tweet
  • Create new GeneratedIdeas and a TweetDraft and saves them to our Postgres DB

Phew! We’re doing it 💪 

Fetching & Displaying Ideas

Defining a Server-side Query

Since we now have our chain of GPT prompts defined via LangChain and our server-side action, let’s go ahead and start implementing some front-end logic to fetch that data and display it to our users… which is basically only us at this point 🫂.

Just as we added a server-side action to generateNewIdeas we will now define a query to fetch those ideas.

Add the following query to your main.wasp file:

query getTweetDraftsWithIdeas {
fn: import { getTweetDraftsWithIdeas } from "@server/ideas.js",
entities: [TweetDraft]
}

In your src/server/ideas.ts file, below your generateNewIdeas action, add the query we just defined in our wasp file:

//... other imports ...
import type { GetTweetDraftsWithIdeas } from '@wasp/queries/types'; // <--- add this ---

// ... other functions ...

type TweetDraftsWithIdeas = {
id: number;
content: string;
notes: string;
createdAt: Date;
originalTweet: {
id: number;
content: string;
tweetId: string;
tweetedAt: Date;
ideas: GeneratedIdea[];
authorUsername: string;
};
}[];

export const getTweetDraftsWithIdeas: GetTweetDraftsWithIdeas<unknown, TweetDraftsWithIdeas> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const drafts = await context.entities.TweetDraft.findMany({
orderBy: {
originalTweet: {
tweetedAt: 'desc',
}
},
where: {
userId: context.user.id,
createdAt: {
gte: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // Get drafts created within the last 2 days
},
},
select: {
id: true,
content: true,
notes: true,
createdAt: true,
originalTweet: {
select: {
id: true,
tweetId: true,
content: true,
ideas: true,
tweetedAt: true,
authorUsername: true,
},
},
},
});

return drafts;
};

With this function we will be returning the tweet drafts we generate, along with our notes, the original tweet that inspired it, and the newly generated ideas.

Sweet!

Ok, but what good is a function that fetches the data if we’ve got nowhere to display it!?

Displaying Ideas Client-side

Let’s go now to our src/client/MainPage.tsx file (make sure it’s got the .tsx extension and not .jsx) and replace the contents with these below:

import waspLogo from './waspLogo.png'
import './Main.css'

const MainPage = () => {
return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
</div>
</div>
)
}
export default MainPage

At this point, you. might need to restart the wasp dev server running in your terminal to get the tailwind configuration to take effect (ctrl + c, then wasp start again).

You’ll now be prompted with the login / register screen. Go ahead and click on register and you will be automatically logged in and redirected to the main page, which at this point only has this:

Untitled

Let’s go back to our MainPage.tsx file and add the magic!

https://media3.giphy.com/media/ekv45izCuyXkXoHRaL/giphy.gif?cid=7941fdc6c3dszwj4xaoxg2kyj6xxdubjxn69m4qruhomhkut&ep=v1_gifs_search&rid=giphy.gif&ct=g

First, let’s create a buttons component so we don’t have to constantly style a new button. Create a new src/client/Button.tsx file:

import { ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}

export default function Button({ isLoading, children, ...otherProps }: ButtonProps) {
return (
<button
{...otherProps}
className={`flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 text-blue-500 font-bold px-3 py-1 text-sm rounded-lg ${isLoading ? ' pointer-events-none opacity-70' : 'cursor-pointer'}`}
>
{isLoading? 'Loading...' : children}
</button>
);
}

Now let’s add it to your AddNote.tsx component, replacing the original button with this one. The whole file should look like this:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';
import Button from './Button';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<Button isLoading={isIdeaEmbedding} onClick={handleEmbedIdea}>
Save Note
</Button>
</div>
);
}

Noice.

Next, we want our page to perform the following actions:

  1. create a button that runs our generateNewIdeas action when clicked
  2. define the query that fetches and caches the tweet drafts and ideas
  3. loop through the results and display them on the page

That’s exactly what the below code will do. Go ahead and replace the MainPage with it and take a minute to review what’s going on:

import waspLogo from './waspLogo.png';
import './Main.css';
import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import AddNote from './AddNote';
import Button from './Button';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
<AddNote />
<hr className='border border-t-1 border-neutral-100/70 w-full' />
<div className='flex flex-row justify-center w-1/4'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</div>
</div>
</div>
);
};
export default MainPage;

This is what you should see on the homepage now! 🎉

Untitled

But, if you clicked ‘generate new ideas’ and nothing happened, well that’s because we haven’t defined any favorite trend-setting twitter users to scrape tweets from. And there’s no way to do that from the UI at the moment, so let’s open up the database manager and add some manually.

In a new terminal tab, in the root of your project, run:

wasp db studio

Then, in a new browswer tab, at localhost:5555 you should see your database.

Go to user, and you should be the only user in there. Add the usernames of a couple of your favorite trend-setting twitter users.

Untitled

Make sure the accounts have tweeted recently or your function won’t be able to scrape or generate anything!

Hey ✋

While you’re at it, if you’re liking this tutorial, give me a follow @hot_town for more future content like this

After adding the twitter usernames, make sure you click save 1 change.

Go back to your client and click the Generate New Ideas button again. This might take a while depending on how many tweets it’s generating ideas for, so be patient — and watch the console output in your terminal if you’re curious ;)

Untitled

Awesome! Now we should be getting back some generated ideas from our twitter “intern” which will help us brainstorm further notes and generate our own BANGER TWEETS.

But it would be cool to also display the tweet these ideas are referencing from the beginning. That way we’d have a bit more context on where the ideas came from.

Let’s do that then! In your MainPage file, at the very top, add the following import:

import { TwitterTweetEmbed } from 'react-twitter-embed';

This allows us to embed tweets with that nice twitter styling.

We already added this dependency to our main.wasp file at the beginning of the tutorial, so we can just import and start embedding tweets.

Let’s try it out now in our MainPage by adding the following snippet above our <h2>Tweet Draft</h2> element:

//...

<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>

<h2>Tweet Draft</h2>
//...

Great. Now we should be sitting pretty 😻

Untitled

You might remember from the beginning of the tutorial when we defined the LLM calls, that if your vector store notes don’t turn back a cosine similarity of at least 0.7, your agent will generate its own ideas entirely without using your notes as a guide.

And since we have NO notes in our vector store at the moment, that’s exactly what it is doing. Which is fine, because we can let it brainstorm for us, and we can select our favorite notes and edit and add them as we see fit.

So you can go ahead and start adding notes whenever you feel like it 📝.

But, we’ve added our favorite twitter users to the database manually. It would be preferable to do it via an account settings page, right? Let’s make one then.

Creating an Account Settings Page

First, add the route and page to your main.wasp config file, under the other routes:

//...

route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
authRequired: true,
component: import Account from "@client/AccountPage"
}

Next, let’s create a new page, src/client/AccountPage.tsx:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
{JSON.stringify(user, null, 2)}
</div>
</div>
);
};

export default AccountPage;

When you navigate to localhost:3000/account, you’ll notice two things, one of them being a logout button. You can see in our SettingsPage above that we imported a Wasp-provided logout function. We get this “for free” since we defined our auth strategy in the main.wasp file — a big time-saver!

Untitled

Because we also defined the AccountPage route with the authRequired: true property, Wasp will automatically pass the logged in user as a prop argument to our page. We can use the user object to display and update our favUsers, just as we can see in the image above.

To do that, let’s define a new updateAccount action in our main.wasp file:

action updateAccount {
fn: import { updateAccount } from "@server/account.js",
entities: [User]
}

Next, let’s create the updateAccount action in a new file, src/server/account.ts:

import type { UpdateAccount } from "@wasp/actions/types";
import HttpError from "@wasp/core/HttpError.js";

export const updateAccount: UpdateAccount<{ favUsers: string[] }, void> = async ({ favUsers }, context) => {
if (!context.user) {
throw new HttpError(401, "User is not authorized");
}

try {
await context.entities.User.update({
where: { id: context.user.id },
data: { favUsers },
});

} catch (error: any) {
throw new HttpError(500, error.message);
}
}

Right. Now it’s time to put it all together in our Account page. We’re going to create a form for adding new twitter users to scrape tweets from, so at the bottom of your src/client/AccountPage.tsx, below your other code, add the following component:

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
//...
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

This component takes care of adding the logged in user’s favUsers array to state, and displaying that in information in a set of input components.

The only thing missing from it is to add our updateAccount action we just defined earlier. So at the top of the file, let’s import it and add the logic to our InputFields submit handler

import updateAccount from '@wasp/actions/updateAccount'; // <--- add this import

//...

const handleSubmit = async () => { // < --- add this function
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

Also, in your AccountPage make sure to replace the line {JSON.stringify(user, null, 2)} with the newly created component <InputFields user={user} />.

Here is what the entire AccountPage.tsx file should now look like in case you get stuck:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';
import updateAccount from '@wasp/actions/updateAccount'

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
<InputFields user={user} />
</div>
</div>
);
};

export default AccountPage;

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

And here’s what your AccountPage should look like when navigating to localhost:3000/account (note: the styling may be a bit ugly, but we’ll take care of that later):

Untitled

Fantastic. So we’ve got the majority of the app logic finished — our own personal twitter “intern” to help us all become thought leaders and thread bois 🤣.

Adding a Cron Job

But wouldn’t it be cool if we could automate the Generate New Ideas process? Each time you click the button, it takes quite a while for tweets to be scraped, and ideas to be generated, especially if we are generating ideas for a lot of new tweets.

So it would be nicer if we had a cron job (recurring task), that ran automatically in the background at a set interval.

With Wasp, that’s also super easy to set up. To do so, let’s go to our main.wasp file and add our job at the very bottom:

//...

job newIdeasJob {
executor: PgBoss,
perform: {
fn: import generateNewIdeasWorker from "@server/worker/generateNewIdeasWorker.js"
},
entities: [User, GeneratedIdea, Tweet, TweetDraft],
schedule: {
// run cron job every 30 minutes
cron: "*/30 * * * *",
executorOptions: {
pgBoss: {=json { "retryLimit": 2 } json=},
}
}
}

Let’s run through the code above:

  • Jobs use pg-boss, a postgres extension, to queue and run tasks under the hood.
  • with perform we’re telling the job what function we want it to call: generateNewIdeasWorker
  • just like actions and queries, we have to tell the job which entities we want to give it access to. In this case, we will need access to all of our entities.
  • the schedule allows us to pass some options to pg-boss so that we can make it a recurring task. In this case, I set it to run every 30 minutes, but you can set it to any interval you’d like (tip: change the comment and let github co-pilot write the cron for you). We also tell pg-boss to retry a failed job two times.

Perfect. So now, our app will automatically scrape our favorite users’ tweets and generate new ideas for us every 30 minutes. This way, if we revisit the app after a few days, all the content will already be there and we won’t have to wait a long time for it to generate it for us. We also make sure we never miss out on generating ideas for older tweets.

But for that to happen, we have to define the function our job will call. To do this, create a new directory worker within the server folder, and within it a new file: src/server/worker/generateNewIdeasWorker

import { generateNewIdeas } from '../ideas.js';

export default async function generateNewIdeasWorker(_args: unknown, context: any) {
try {
console.log('Running recurring task: generateNewIdeasWorker')
const allUsers = await context.entities.User.findMany({});

for (const user of allUsers) {
context.user = user;
console.log('Generating new ideas for user: ', user.username);
await generateNewIdeas(undefined as never, context);
console.log('Done generating new ideas for user: ', user.username)
}

} catch (error: any) {
console.log('Recurring task error: ', error);
}
}

In this file, all we’re doing is looping through all the users in our database, and passing them via the context object to our generateNewIdeas action. The nice thing about jobs is that Wasp automatically passes the context object to these functions, which we can then pass along to our action.

So now, at the interval that you set (e.g. 30 minutes), you should notice the logs being printed to the console whenever your job starts automatically running.

[Server]  Generating new ideas for user:  vinny

Alright, things are looking pretty good now, but let’s not forget to add a page to view all the notes we added and embedded to our vector store!

Adding a Notes Page

Go ahead and add the following route to your main.wasp file:

route NotesPage { path: "/notes", to: NotesPage }
page NotesPage {
authRequired: true,
component: import Notes from "@client/NotesPage"
}

Create the complementary page, src/client/NotesPage.tsx and add the following boilerplate just to get started (we’ll add the rest later):

const NotesPage = () => {

return (
<>Notes</>
);
};

export default NotesPage;

It would be nice if we had a simple Nav Bar to navigate back and forth between our two pages. It would also be cool if we had our <AddNote /> input component on all pages, that way it’s easy for us to add an idea whenever inspiration strikes.

Rather than copying the NavBar and AddNote code to both pages, let’s create a wrapper, or “root”, component for our entire app so that all of our pages have the same Nav Bar and layout.

To do that, in our main.wasp file, let’s define our root component by adding a client property to our app configuration at the very top of the file. This is how the entire app object should look like now:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
client: {
rootComponent: import App from "@client/App",
},
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// entities, operations, routes, and other stuff...

Next, create a new file src/client/App.tsx with the following content:

import './Main.css';
import AddNote from './AddNote';
import { ReactNode } from 'react';
import useAuth from '@wasp/auth/useAuth';

const App = ({ children }: { children: ReactNode }) => {

const { data: user } = useAuth();

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<div className='flex flex-row justify-between items-center w-1/2 mb-6 text-neutral-600 px-2'>
<div className='flex justify-start w-1/3'>
<a href='/' className='hover:underline cursor-pointer'>
🤖 Generated Ideas
</a>
</div>
<div className='flex justify-center w-1/3'>
<a href='/notes' className='hover:underline cursor-pointer'>
📝 My Notes
</a>
</div>
<div className='flex justify-end w-1/3'>
<a href='/account' className='hover:underline cursor-pointer'>
👤 Account
</a>
</div>
</div>

<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
{!!user && <AddNote />}
<hr className='border border-t-1 border-neutral-100/70 w-full' />
{children}
</div>
</div>
</div>
);
};

export default App;

With this defined, Wasp will know to pass all other routes as children through our App component. That way, we will always show the Nav Bar and AddNote component on the top of every page.

We also take advantage of Wasp’s handy useAuth hook to check if a user is logged in, and if so we show the AddNote component.

Now, we can delete the duplicate code on our MainPage. This is what it should look like now:

import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import Button from './Button';
import { TwitterTweetEmbed } from 'react-twitter-embed';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<>
<div className='flex flex-row justify-center w-full'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</>
);
};
export default MainPage;

Next, we need to create a query that allows us to fetch all of our added notes and ideas that have been embedded in our vector store.

For that, we need to define a new query in our main.wasp file:

query getEmbeddedNotes {
fn: import { getEmbeddedNotes } from "@server/ideas.js",
entities: [GeneratedIdea]
}

We then need to create that query at the bottom of our src/actions/ideas.ts file:

// first import the type at the top of the file
import type { GetEmbeddedNotes, GetTweetDraftsWithIdeas } from '@wasp/queries/types';

//...

export const getEmbeddedNotes: GetEmbeddedNotes<never, GeneratedIdea[]> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const notes = await context.entities.GeneratedIdea.findMany({
where: {
userId: context.user.id,
isEmbedded: true,
},
orderBy: {
createdAt: 'desc',
},
});

return notes;
}

Now let’s go back to our src/client/NotesPage.tsx and add our query. Our new file will look like this:

import { useQuery } from '@wasp/queries';
import getEmbeddedNotes from '@wasp/queries/getEmbeddedNotes';

const NotesPage = () => {
const { data: notes, isLoading, error } = useQuery(getEmbeddedNotes);

if (isLoading) <div>Loading...</div>;
if (error) <div>Error: {error.message}</div>;

return (
<>
<h2 className='text-2xl font-bold'>My Notes</h2>
{notes && notes.length > 0 ? (
notes.map((note) => (
<div key={note.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{note.content}</div>
</div>
</div>
))
) : notes && notes.length === 0 && (
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>No notes yet</div>
</div>
)}
</>
);
};

export default NotesPage;

Cool! Now we should be fetching all our embedded notes and ideas, signified by the isEmbedded tag in our postgres database. Your Notes page should now look something like this:

Untitled

You Did it! Your own Twitter Intern 🤖

Help me help you

🌟 If you haven’t yet, please star us on GitHub, especially if you found this useful! If you do, it helps support us in creating more content like this. And if you don’t… well, we will deal with it, I guess.

https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif

And that’s it! You’ve now got yourself a semi-autonomous twitter brainstorming agent to help inspire new ideas and keep you actively contributing 🚀

There’s way more you can do with these tools, but this is a great start.

Remember, if you want to see a more advanced version of this app which utilizes the official Twitter API to send tweets, gives you the ability to edit and add generated notes on the fly, has manual similarity search for all your notes, and more, then you can check out the 💥 Banger Tweet Bot 🤖.

And, once again, here's the repo for the finished app we built in this tutorial: Personal Twitter Intern

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/28/what-can-you-build-with-wasp.html b/blog/2023/06/28/what-can-you-build-with-wasp.html index 69545afdf0..be5e7620f5 100644 --- a/blog/2023/06/28/what-can-you-build-with-wasp.html +++ b/blog/2023/06/28/what-can-you-build-with-wasp.html @@ -19,13 +19,13 @@ - - + +
-

What can you build with Wasp?

· 4 min read
Matija Sosic

Launch Week 3 is coming

Welcome to the 3rd day of our Launch Week #3 - Community Day! Our community is the most important aspect of everything we do at Wasp, and we believe it's only right to have a day dedicated to it.

We'll showcase some of the coolest project built with Wasp so far and through that explore together what kind of apps you can develop with it. Let's dive in!

tip

If you're looking for a quick way to start your project, check out our Ultimate SaaS Starter. It packs Tailwind, GPT, Stripe ane other popular integrations, all pre-configured for you.

CoverLetterGPT.xyz - GPT-powered cover letter generator

Try it out: coverlettergpt.xyz

Source code: https://github.com/vincanger/coverlettergpt

Wasp features used: Social login with Google + auth UI, email sending

UI Framework: Chakra UI

Created in the midst of a GPT craze, this is one of the most popular Wasp apps so far! It does exactly what it says on a tin - given job description and your CV, it generates a unique cover letter customized for you. It does that via parsing your CV and feeding it together with the job description to the GPT api, along with the additional settings such as creativity level (careful with that one!).

Although it started as a fun side project, it seems that people actually find it useful, at least as a starting point for writing your own cover letter. CoverLetterGPT has been used to generate close to 5,000 cover letters!

Try it out and have fun or use it as an inspiration for your next project!

Amicus.work - most "enterprise SaaS" app 👔 💼

Try it out: amicus.work

Wasp features used: Authentication, email sending, async/cron jobs

UI Framework: Material UI

This app really gives away those "enterprise SaaS" vibes - when you see it you know it means some serious business! The author describes it as "Asana for you lawyers" (you can read how the author got first customers for it here), or as an easy way for lawyers to manage and collaborate on their workflows.

File upload, workflow creation, calendar integration, collaboration - this app has it all! Amicus might be the most advanced project made with Wasp so far. Erlis startedbuilding it even with Wasp still in Alpha, and it has withstood the test of time since then.

Description Generator - GPT-powered product description generator - first acquired app made with Wasp! 💰💰

Try it out: description-generator.online

Wasp features used: Social login with Google + auth UI

UI Framework: Chakra UI

Another SaaS that uses GPT integration to cast its magic! Given product name and instructions on what kind of content you'd like to get, this app generates the professionaly written product listing. It's a perfect fit for marketplace owners that want to present their products in the best light but don't have a budget for the marketing agency.

What's special about Description Generator is that it was recently sold , making it the first Wasp-powered project that got acquired! Stay tuned, as the whole story is coming soon.

TweetBot - your personal Twitter intern! 🐦🤖

Try it out: banger-tweet-bot.netlify.app

Source code: https://github.com/vincanger/banger-tweet-bot

Wasp features used:Authentication, async/cron jobs

UI Framework: Tailwind

The latest and greatest from Vince's lab - an app that serves as your personal twitter brainstorming agent! It takes your raw ideas as an input, monitors current twitter trends (from the accounts you selected) and helps you brainstorm new tweets and also drafts them for you!

While the previously mentioned projects queried the GPT API directly, TweetBot makes use of the LangChain library, which does a lot of heavy lifting for you, allowing you to produce bigger prompts and preserve the context between subsequent queries.

Summary

As you could see above, Wasp can be used to build pretty much any database-backed web application! It is especially well suited for so called "workflow-based" applications where you typically have a bunch of resources (e.g. your tasks, or tweets) that you want to manipulate in some way.

With our built-in deployment support (e.g. you can deploy to Fly.io for free with a single CLI command) the whole development process is extremely streamlined.

We can't wait to see what you build next!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

What can you build with Wasp?

· 4 min read
Matija Sosic

Launch Week 3 is coming

Welcome to the 3rd day of our Launch Week #3 - Community Day! Our community is the most important aspect of everything we do at Wasp, and we believe it's only right to have a day dedicated to it.

We'll showcase some of the coolest project built with Wasp so far and through that explore together what kind of apps you can develop with it. Let's dive in!

tip

If you're looking for a quick way to start your project, check out our Ultimate SaaS Starter. It packs Tailwind, GPT, Stripe ane other popular integrations, all pre-configured for you.

CoverLetterGPT.xyz - GPT-powered cover letter generator

Try it out: coverlettergpt.xyz

Source code: https://github.com/vincanger/coverlettergpt

Wasp features used: Social login with Google + auth UI, email sending

UI Framework: Chakra UI

Created in the midst of a GPT craze, this is one of the most popular Wasp apps so far! It does exactly what it says on a tin - given job description and your CV, it generates a unique cover letter customized for you. It does that via parsing your CV and feeding it together with the job description to the GPT api, along with the additional settings such as creativity level (careful with that one!).

Although it started as a fun side project, it seems that people actually find it useful, at least as a starting point for writing your own cover letter. CoverLetterGPT has been used to generate close to 5,000 cover letters!

Try it out and have fun or use it as an inspiration for your next project!

Amicus.work - most "enterprise SaaS" app 👔 💼

Try it out: amicus.work

Wasp features used: Authentication, email sending, async/cron jobs

UI Framework: Material UI

This app really gives away those "enterprise SaaS" vibes - when you see it you know it means some serious business! The author describes it as "Asana for you lawyers" (you can read how the author got first customers for it here), or as an easy way for lawyers to manage and collaborate on their workflows.

File upload, workflow creation, calendar integration, collaboration - this app has it all! Amicus might be the most advanced project made with Wasp so far. Erlis startedbuilding it even with Wasp still in Alpha, and it has withstood the test of time since then.

Description Generator - GPT-powered product description generator - first acquired app made with Wasp! 💰💰

Try it out: description-generator.online

Wasp features used: Social login with Google + auth UI

UI Framework: Chakra UI

Another SaaS that uses GPT integration to cast its magic! Given product name and instructions on what kind of content you'd like to get, this app generates the professionaly written product listing. It's a perfect fit for marketplace owners that want to present their products in the best light but don't have a budget for the marketing agency.

What's special about Description Generator is that it was recently sold , making it the first Wasp-powered project that got acquired! Stay tuned, as the whole story is coming soon.

TweetBot - your personal Twitter intern! 🐦🤖

Try it out: banger-tweet-bot.netlify.app

Source code: https://github.com/vincanger/banger-tweet-bot

Wasp features used:Authentication, async/cron jobs

UI Framework: Tailwind

The latest and greatest from Vince's lab - an app that serves as your personal twitter brainstorming agent! It takes your raw ideas as an input, monitors current twitter trends (from the accounts you selected) and helps you brainstorm new tweets and also drafts them for you!

While the previously mentioned projects queried the GPT API directly, TweetBot makes use of the LangChain library, which does a lot of heavy lifting for you, allowing you to produce bigger prompts and preserve the context between subsequent queries.

Summary

As you could see above, Wasp can be used to build pretty much any database-backed web application! It is especially well suited for so called "workflow-based" applications where you typically have a bunch of resources (e.g. your tasks, or tweets) that you want to manipulate in some way.

With our built-in deployment support (e.g. you can deploy to Fly.io for free with a single CLI command) the whole development process is extremely streamlined.

We can't wait to see what you build next!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/29/new-wasp-lsp.html b/blog/2023/06/29/new-wasp-lsp.html index be6d6220df..1b472244aa 100644 --- a/blog/2023/06/29/new-wasp-lsp.html +++ b/blog/2023/06/29/new-wasp-lsp.html @@ -19,13 +19,13 @@ - - + +
-

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

· 2 min read
Matija Sosic

It's the fourth day of our Launch Week #3 - today it's all about dev tooling and making sure that the time you spend looking at your IDE is as pleasurable as possible!

We present the next generation of Wasp LSP (Language Server Protocol) implementation for VS Code! As you might already know, Wasp has its own simple configuration language (.wasp) that acts as a glue between your React & Node.js code.

Although it's a very simple, declarative language (you can think of it as a bit nicer/smarter JSON), and having it allows us to completely tailor the developer experience (aka get rid of boilerplate), it also means we have to provide our own tooling for it (syntax highlighting, auto completion, ...).

We started with syntax highlighting, then basic autocompletion and snippet support, but now we really took things to the next level! Writing Wasp code now is much closer to what we had in our mind when envisioning Wasp.

Without further ado, here's what's new:

✨ Autocompletion for config object properties (auth, webSocket, ...)

Until now, Wasp offered autocompletion only for the top-level declarations such as page or app. Now, it works for any (sub)-property (as one would expect 😅)!

Fill out your Wasp configuration faster and with less typos! 💻🚀

🔍 Type Hints

Opening documentation takes you out of your editor and out of your flow. Stay in the zone with in-editor type hints! 💡

🚨 Import Diagnostics

Keep tabs on what's left to implement with JS import diagnostics! There's nothing more satisfying than watching those errors vanish. 😌

Wasp now automatically detects if the function you referenced doesn't exist or is not exported.

🔗 Goto Definition

Your Wasp file is the central hub of your project. Easily navigate your code with goto definition and make changes in a snap! 💨

Cmd/Ctrl + click and Wasp LSP takes you straight to the function body!

Don't forget to install Wasp VS Code extension and we wish you happy coding! You can get started right away and try it out here.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

· 2 min read
Matija Sosic

It's the fourth day of our Launch Week #3 - today it's all about dev tooling and making sure that the time you spend looking at your IDE is as pleasurable as possible!

We present the next generation of Wasp LSP (Language Server Protocol) implementation for VS Code! As you might already know, Wasp has its own simple configuration language (.wasp) that acts as a glue between your React & Node.js code.

Although it's a very simple, declarative language (you can think of it as a bit nicer/smarter JSON), and having it allows us to completely tailor the developer experience (aka get rid of boilerplate), it also means we have to provide our own tooling for it (syntax highlighting, auto completion, ...).

We started with syntax highlighting, then basic autocompletion and snippet support, but now we really took things to the next level! Writing Wasp code now is much closer to what we had in our mind when envisioning Wasp.

Without further ado, here's what's new:

✨ Autocompletion for config object properties (auth, webSocket, ...)

Until now, Wasp offered autocompletion only for the top-level declarations such as page or app. Now, it works for any (sub)-property (as one would expect 😅)!

Fill out your Wasp configuration faster and with less typos! 💻🚀

🔍 Type Hints

Opening documentation takes you out of your editor and out of your flow. Stay in the zone with in-editor type hints! 💡

🚨 Import Diagnostics

Keep tabs on what's left to implement with JS import diagnostics! There's nothing more satisfying than watching those errors vanish. 😌

Wasp now automatically detects if the function you referenced doesn't exist or is not exported.

🔗 Goto Definition

Your Wasp file is the central hub of your project. Easily navigate your code with goto definition and make changes in a snap! 💨

Cmd/Ctrl + click and Wasp LSP takes you straight to the function body!

Don't forget to install Wasp VS Code extension and we wish you happy coding! You can get started right away and try it out here.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/30/tutorial-jam.html b/blog/2023/06/30/tutorial-jam.html index c4726221b7..61f4586908 100644 --- a/blog/2023/06/30/tutorial-jam.html +++ b/blog/2023/06/30/tutorial-jam.html @@ -19,15 +19,15 @@ - - + +
-

Tutorial Jam #1 - Teach Others & Win Prizes!

· 4 min read
Vinny

Introduction

The Wasp Tutorial Jam is a contest where participants are required to create a tutorial about building a fullstack React/Node app with Wasp.

Wait, What’s Wasp?

First of all, it’s sad that you’ve never heard of Wasp.

https://media0.giphy.com/media/kr5PszPQawIRq/giphy.gif?cid=7941fdc6gwgjf866b0akslgciedh53jf9narttadkglvvcp0&ep=v1_gifs_search&rid=giphy.gif&ct=g

Wasp is a unique fullstack framework for building React/NodeJS/Prisma/Tanstack Query apps.

Because it’s based on a compiler, you write a simple config file, and Wasp can take care of generating the skeleton of your app for you (and regenerating when the config file changes). You can read more about Wasp here

Rules

The rules are simple. The tutorial must:

- - + + \ No newline at end of file diff --git a/blog/2023/07/10/gpt-web-app-generator.html b/blog/2023/07/10/gpt-web-app-generator.html index 895d58afbb..e41fd80471 100644 --- a/blog/2023/07/10/gpt-web-app-generator.html +++ b/blog/2023/07/10/gpt-web-app-generator.html @@ -19,14 +19,14 @@ - - + +
-

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

· 6 min read
Martin Sosic

This project started out as an experiment - we were interested if, given a short description, GPT can generate a full-stack web app in React & Node.js. The results went beyond our expectations!

How it works

All you have to do in order to use GPT Web App Generator is provide a short description of your app idea in plain English. You can optionally select your app's brand color and the preferred authentication method (more methods coming soon).

1. Describe your app 2. Pick the color 3. Generate your app 🚀

That's it - in a matter of minutes, a full-stack web app codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available for you to download, run it locally and deploy with a single CLI command!

See a full one-minute demo here:


Check out this blog post if you are interested in technical details of how implemented the Generator!

The stack 📚

Besides React & Node.js, GPT Web App Generator uses Prisma and Wasp.

Prisma is a type-safe database ORM built on top of PostgreSQL. It makes it easy to deal with data models and database migrations.

Wasp is a batteries-included, full-stack framework for React & Node.js. It takes care of everything from front-end to back-end and database along with authentication, sending emails, async jobs, deployment, and more.

Additionaly, all the code behind GPT Web App Generator is completely open-source: web app, GPT code agent.

What kind of apps can I build with it?

caution

Since this is a GPT-powered project, it's output is not 100% deterministic and small mistakes will sometimes occur in the generated code. For the typical examples of web apps (as seen below) they are usually very minor and straightforward to fix. +

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

· 6 min read
Martin Sosic

This project started out as an experiment - we were interested if, given a short description, GPT can generate a full-stack web app in React & Node.js. The results went beyond our expectations!

How it works

All you have to do in order to use GPT Web App Generator is provide a short description of your app idea in plain English. You can optionally select your app's brand color and the preferred authentication method (more methods coming soon).

1. Describe your app 2. Pick the color 3. Generate your app 🚀

That's it - in a matter of minutes, a full-stack web app codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available for you to download, run it locally and deploy with a single CLI command!

See a full one-minute demo here:


Check out this blog post if you are interested in technical details of how implemented the Generator!

The stack 📚

Besides React & Node.js, GPT Web App Generator uses Prisma and Wasp.

Prisma is a type-safe database ORM built on top of PostgreSQL. It makes it easy to deal with data models and database migrations.

Wasp is a batteries-included, full-stack framework for React & Node.js. It takes care of everything from front-end to back-end and database along with authentication, sending emails, async jobs, deployment, and more.

Additionaly, all the code behind GPT Web App Generator is completely open-source: web app, GPT code agent.

What kind of apps can I build with it?

caution

Since this is a GPT-powered project, it's output is not 100% deterministic and small mistakes will sometimes occur in the generated code. For the typical examples of web apps (as seen below) they are usually very minor and straightforward to fix. If you get stuck, ping us on our Discord.

The generated apps are full-stack and consist of front-end, back-end and database. Here are few of the examples we successfully created:

My Plants - track your plants' watering schedule 🌱🚰

  • See the generated code and run it yourself here

This app does exactly what it says - makes sure that you water your plants on time! It comes with a fully functioning front-end, back-end and the database with User and Plant entities. It also features a full-stack authentication (username & password) and a Tailwind-based design.

The next step would be to add more advanced features, such as email reminders (via Wasp email sending support) when it is time to water your plant.

You can see and download the entire source code and add more features and deploy the app yourself!

ToDo app - a classic ✅

  • See the generated code and run it yourself here

What kind of a demo would this be if it didn't include a ToDo app? GPT Web App Generator successfully scaffolded it, along with all the basic functionality - creating and marking a task as done.

With the foundations in place (full-stack code, authentication, Tailwind CSS design) you can see & download the code here and try it yourself!

Limitations

In order to reduce the complexity and therefore mistakes GPT makes, for this first version of Generator we went with the following limitations regarding generated apps:

  1. No additional npm dependencies.
  2. No additional files beyond Wasp Pages (React) and Operations (Node). So no additional files with React components, CSS, utility JS, images or similar.
  3. No TypeScript, just Javascript.
  4. No advanced Wasp features (e.g. Jobs, Auto CRUD, Websockets, Social Auth, email sending, …).

Summary & next steps

As mentioned above, our goal was to test whether GPT can be effectively used to generate full-stack web applications with React & Node.js. While it's now obvious it can, we have lot of ideas for new features and improvements.

Challenges

While we were expecting the main issue to be the size of context that GPT has, it turned out to be that the bigger issue is its “smarts”, which determine things like its planning capabilities, capacity to follow provided instructions (we had quite some laughs observing how it sometimes ignores our instructions), and capacity to not do silly mistakes. We saw GPT4 give better results than GPT3.5, but both still make mistakes, and GPT4 is also quite slow/expensive. Therefore we are quite excited about the further developments in the field of AI / LLMs, as they will directly affect the quality of the output for the tools like our Generator.

Next features wishlist

  1. Get feedback on this initial experiment - both on the Generator and the Wasp as a framework itself: best place to leave us feedback is on our Discord.
  2. Further improve code agent & web app.
  3. Release new version of wasp CLI that allows generating new Wasp project by providing short description via CLI. Our code agent will then use GPT to generate project on the disk. This is already ready and should be coming out soon.
  4. Also allow Wasp users to use code agent for scaffolding specific parts of their Wasp app → you want to add a new Wasp Page (React)? Run our code agent via Wasp CLI or via Wasp vscode extension and have it generated for you, with initial logic already implemented.
  5. As LLMs progress, try some alternative approaches, e.g. try fine-tuning an LLM with knowledge about Wasp, or give LLM more freedom while generating files and parts of the codebase.
  6. Write a detailed blog post about how we implemented the Generator, which techniques we used, how we designed our prompts, what worked and what didn’t work, … .

Support us! ⭐️

If you wish to express your support for what we are doing, consider giving us a star on Github! Everything we do at Wasp is open source, and your support motivates us and helps us to keep making web app development easier and with less boilerplate.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/07/17/how-we-built-gpt-web-app-generator.html b/blog/2023/07/17/how-we-built-gpt-web-app-generator.html index df19b00fde..b2e52123e3 100644 --- a/blog/2023/07/17/how-we-built-gpt-web-app-generator.html +++ b/blog/2023/07/17/how-we-built-gpt-web-app-generator.html @@ -19,15 +19,15 @@ - - + +
-

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

· 23 min read
Martin Sosic

We created GPT Web App Generator, which lets you shortly describe the web app you would like to create, and in a matter of minutes, a full-stack codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available to download and run locally!

We started this as an experiment, to see how well we could use GPT to generate full-stack web apps in Wasp, the open-source JS web app framework that we are developing. Since we launched, we had more than 3000 apps generated in just a couple of days!

1. Describe your app 2. Pick the color 3. Generate your app 🚀

Check out this blog post to see GPT Web App Generator in action, including a one-minute demo video, few example apps, and learn a bit more about our plans for the future. Or, try it out yourself at https://magic-app-generator.wasp-lang.dev/ !

In this blog post, we are going to explore the technical side of creating the GPT Web App Generator: techniques we used, how we engineered our prompts, challenges we encountered, and choices we made! (Note from here on we will just refer to it as the “Generator”, or “code agent” when talking about the backend)

Also, all the code behind the Generator is open source: web app, GPT code agent.

How well does it work 🤔?

First, let’s quickly explain what we ended up with and how it performs.

Input into our Generator is the app name, app description (free form text), and a couple of simple options such as primary app color, temperature, auth method, and GPT model to use.

Input for generating a Todo app

As an output, Generator spits out the whole JS codebase of a working full-stack web app: frontend, backend, and database. Frontend is React + Tailwind, the backend is NodeJS with Express, and for working with the database we used Prisma. This is all connected together with the Wasp framework.

You can see an example of generated codebase here: https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f .

Result of generating a Todo app

Generator does its best to produce code that works out of the box → you can download it to your machine and run it. For simpler apps, such as TodoApp or MyPlants, it often generates code with no mistakes, and you can run them out of the box.

What generated TodoApp looks like

For a bit more complex apps, like a blog with posts and comments, it still generates a reasonable codebase but there are some mistakes to be expected here and there. For even more complex apps, it usually doesn’t follow up completely, but stops at some level of complexity and fills in the rest with TODOs or omits functionality, so it is kind of like a simplified model of what was asked for. Overall, it is optimized for producing CRUD business web apps.

This makes it a great tool for kick-starting your next web app project with a solid prototype, or to even generate working, simple apps on the fly!

How does it work ⚙️?

When we set out to build the Generator, we gave ourselves the following goals:

  • we must be able to build it in a couple of weeks
  • it has to be relatively easy to maintain in the future
  • it needs to generate the app quickly and cheaply (a couple of minutes, < $1)
  • generated apps should have as few mistakes as possible

Therefore, to keep it simple, we don’t do any LLM-level engineering or fine-tuning, instead, we just use OpenAI API (specifically GPT3.5 and GPT4) to generate different parts of the app while giving it the right context at every moment (pieces of docs, examples, guidelines, …). To ensure the coherence and quality of the generated app, we don’t give our code agent too much freedom but instead heavily guide it, step by step, through generating the app.

As step zero, we generate some code files deterministically, without GPT, just based on the options that the user chose (primary color, auth method): those include some config files for the project, some basic global CSS, and some auth logic. You can see this logic here (we call those “skeleton” files): code on Github .

Then, the code agent takes over!

The code agent does its work in 3 main phases:

  1. Planning 📝
  2. Generating 🏭
  3. Fixing 🔧

Since GPT4 is quite slower and significantly more expensive than GPT3.5 (also has a lower rate limit regarding the number of tokens per minute, and also the number of requests per minute), we use GPT4 only for the planning, since that is the crucial step, and then after that, we use GPT3.5 for the rest.

As for cost per app 💸: one app typically consumes from 25k to 60k tokens, which comes to about $0.1 to $0.2 per app, when we use a mix of GPT4 and GPT3.5. If we run it just with GPT4, then the cost is 10x, which is from $1 to $2.

🎶 Intermezzo: short explanation of OpenAI Chat Completions API

OpenAI API offers different services, but we used only one of them: “chat completions”.

API itself is actually very simple: you send over a conversation, and you get a response from the GPT.

The conversation is just a list of messages, where each message has content and a role, where the role specifies who “said” that content → was it “user” (you), or “assistant” (GPT).

The important thing to note is that there is no concept of state/memory: every API call is completely standalone, and the only thing that GPT knows about is the conversation you provide it with at that moment!

If you are wondering how ChatGPT (the web app that uses GPT in the background) works with no memory → well, each time you write a message, the whole conversation so far is resent again! There are some additional smart mechanisms in play here, but that is really it at its core.

Official guide, official API reference.

Step #1: Planning 📝

A Wasp app consists of Entities (Prisma data models), Operations (NodeJS Queries and Actions), and Pages (React).

Once given an app description and title, the code agent first generates a Plan: it is a list of Entities, Operations (Queries and Actions), and Pages that comprise the app. So kind of like an initial draft of the app. It doesn’t generate the code yet → instead, it comes up with their names and some other details, including a short description of what they should behave like.

This is done via a single API request toward GPT, where the prompt consists of the following:

  • Short info about the Wasp framework + an example of some Wasp code.
  • We explain that we want to generate the Plan, explain what it is, and how it is represented as JSON, by describing its schema.
  • We provide some examples of the Plan, represented as JSON.
  • Some rules and guidelines we want it to follow (e.g. “plan should have at least 1 page”, “make sure to generate a User entity”).
  • Instructions to return the Plan only as a valid JSON response, and no other text.
  • App name and description (as provided by the user).

You can see how we generate such a prompt in the code here.

Also, here is an actual instance of this prompt for a TodoApp.
Wasp is a full-stack web app framework that uses React (for client), NodeJS and Prisma (for server).
High-level of the app is described in main.wasp file (which is written in special Wasp DSL), details in JS/JSX files.
Wasp DSL (used in main.wasp) reminds a bit of JSON, and doesn't use single quotes for strings, only double quotes. Examples will follow.

Important Wasp features:
- Routes and Pages: client side, Pages are written in React.
- Queries and Actions: RPC, called from client, execute on server (nodejs).
Queries are for fetching and should not do any mutations, Actions are for mutations.
- Entities: central data models, defined via PSL (Prisma schema language), manipulated via Prisma.
Typical flow: Routes point to Pages, Pages call Queries and Actions, Queries and Actions work with Entities.

Example main.wasp (comments are explanation for you):

```wasp
app todoApp {
wasp: { version: "^0.11.1" },
title: "ToDo App",
auth: {
userEntity: User,
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login"
},
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
db: {
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx"
}

route DashboardRoute { path: "/", to: Dashboard }
page DashboardPage {
authRequired: true,
component: import Dashboard from "@client/pages/Dashboard.jsx"
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
psl=}

entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

query getUser {
fn: import { getUser } from "@server/queries.js",
entities: [User] // Entities that this query operates on.
}

query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}

action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}

action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
```

We are looking for a plan to build a new Wasp app (description at the end of prompt).

Instructions you must follow while generating plan:
- App uses username and password authentication.
- App MUST have a 'User' entity, with following fields required:
- `id Int @id @default(autoincrement())`
- `username String @unique`
- `password String`
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
- One of the pages in the app must have a route path "/".
- Don't generate the Login or Signup pages and routes under any circumstances. They are already generated.

Plan is represented as JSON with the following schema:

{
"entities": [{ "entityName": string, "entityBodyPsl": string }],
"actions": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"queries": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"pages": [{ "pageName": string, "componentPath": string, "routeName": string, "routePath": string, "pageDesc": string }]
}

Here is an example of a plan (a bit simplified, as we didn't list all of the entities/actions/queries/pages):

{
"entities": [{
"entityName": "User",
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]"
}],
"actions": [{
"opName": "createTask",
"opFnPath": "@server/actions.js",
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
}],
"queries": [{
"opName": "getTask",
"opFnPath": "@server/queries.js",
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
}],
"pages": [{
"pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
}]
}

We will later use this plan to write main.wasp file and all the other parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Also, mention in the descriptions of actions/queries which entities they work with,
and in descriptions of pages mention which actions/queries they use.

Typically, plan will have AT LEAST one query, at least one action, at least one page, and at
least two entities. It will very likely have more than one of each, though.

DO NOT create actions for login and logout under any circumstances. They are already included in Wasp.

Note that we are using SQLite as a database for Prisma, so don't use scalar arrays in PSL, like `String[]`,
as those are not supported in SQLite. You can of course normally use arrays of other models, like `Task[]`.

Please, respond ONLY with a valid JSON that is a plan.
There should be no other text in the response.

==== APP DESCRIPTION: ====

App name: TodoApp
A simple todo app with one main page that lists all the tasks. User can create new tasks by providing their description, toggle existing ones, or edit their description. User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.

GPT then responds with a JSON (hopefully), that we parse, and we have ourselves a Plan! We will use this Plan in the following steps, to drive our generation of other parts of the app. Note that GPT sometimes adds text to the JSON response or returns invalid JSON, so we built in some simple approaches to overcome these issues, which we explain in detail later.

🎶 Intermezzo: Common prompt design

The prompt design we just described above for generating a Plan is actually very similar for other steps (e.g. the Generation and Fixing steps along with their respective sub-steps), so let’s cover those commonalities.

All of the prompts we use more or less adhere to the same basic structure:

  • General context
    • Short info about what Wasp framework is.
    • Doc snippets (with code examples if needed) about whatever we are generating right now (e.g. examples of NodeJS code, or examples of React code).
  • Project context: stuff we generated in the previous steps that is relevant to the current step.
  • Instructions on what we want to generate right now + JSON schema for it + example of such JSON response.
  • Rules and guidelines: this is a good place to warn it about common mistakes it makes, or give it some additional advice, and emphasize what needs to happen and what must not happen.
  • Instructions to respond only with a valid JSON, and no other text.
  • Original user prompt: app name and description (as provided by the user).

We put the original user prompt at the end because then we can tell GPT in the system message after it sees the start of the original user prompt (we have a special header for it), that it needs to treat everything after it as an app description and not as instructions on what to do → this way we attempt to defend from the potential prompt injection.

Step #2: Generating 🏭

After producing the Plan, Generator goes step by step through the Plan and asks GPT to generate each web app piece, while providing it with docs, examples, and guidelines. Each time a web app piece is generated, Generator fits it into the whole app. This is where most of our work comes in: equipping GPT with the right information at the right moment.

In our case, we do it for all the Operations in the Plan (Actions and Queries: NodeJs code), and also for all the Pages in the Plan (React code), with one prompt for each. So if we have 2 queries, 3 actions, and 2 pages, that will be 2+3+2 = 7 GPT prompts/requests. Prompts are designed as explained previously.

Code on Github: generating an Operation, generating a Page.

When generating Operations, we provide GPT with the info about the previously generated Entities, while when generating Pages, we provide GPT with the info about previously generated Entities and Operations.

Step #3: Fixing 🔧

Finally, the Generator tries its best to fix any mistakes that GPT might have introduced previously. GPT loves fixing stuff it previously generated → if you first ask it to generate some code, and then just tell it to fix it, it will often improve it!

To enhance this process further, we don’t just ask it to fix previous code, but also provide it with instructions on what to keep an eye out for, like common types of mistakes that we noticed it often does, and also point it to any specific mistakes we were able to detect on our own.

Regarding detecting mistakes to report to GPT, ideally, you would have a full REPL going on → that means running the generated code through an interpreter/compiler, then sending it for repairs, and so on until all is fixed.

In our case, running the whole project through the TypeScript compiler was not feasible for us with the time limits we put on ourselves, but we used some simpler static analysis tools like Wasp’s compiler (for the .wasp file) and prisma format for Prisma model schemas, and sent those to GPT to fix them. We also wrote some simple heuristics of our own that are able to detect some of the common mistakes.

Our code (& prompt) for fixing a Page.

Our code (& prompt) for fixing Operations.

In the prompt, we would usually repeat the same guidelines we provided previously in the Generation step, while also adding a couple of additional pointers to common mistakes, and that usually helps, it fixes stuff it missed before. But, often not everything, instead something will still get through. Some things we just couldn’t get it to fix consistently, for example, Wasp-specific JS imports, no matter how much we emphasized what it needed to do with them, it would just keep messing them up. Even GPT4 wasn’t perfect in this situation. For such situations, when possible, we ended up writing our own heuristics that would fix those mistakes (fixing JS imports).

Things we tried/learned

Explanations 💬

We tried telling GPT to explain what it did while fixing mistakes: which mistakes it will fix, and which mistakes it fixed, since we read that that can help, but we didn’t see visible improvement in its performance.

Testing 🧪

Testing the performance of your code agent is hard.

In our case, it takes a couple of minutes for our code agent to generate a new app, and you need to run tests directly with the OpenAI API. Also, since results are non-deterministic, it can be pretty hard to say if output was affected by the changes you did or not.

Finally, evaluating the output itself can be hard (especially in our case when it is a whole full-stack web app).

Ideally, we would have set up a system where we can run only parts of the whole generation process, and we could automatically run a specific part a number of times for each of different sets of parameters (which would include different prompts, but also parameters like type of model (gpt4 vs gpt3.5), temperature and similar), in order to compare performance for each of those parameter sets.

Evaluation performance would also ideally be automated, e.g. we would count the mistakes during compilation and/or evaluate the quality of app design → but this is also quite hard.

We, unfortunately, didn’t have time to set up such a system, so we were mostly doing testing manually, which is quite subjective and vulnerable to randomness, and is effective only for changes that have quite a big impact, while you can’t really detect those that are minor optimizations.

Context vs smarts 🧠

When we started working on the Generator, we thought the size of GPT’s context would be the main issue. However, we didn’t have any issues with context at the end → most of what we wanted to specify would fit into 2k to max 4k tokens, while GPT3.5 has context up to 16k!

Instead, we had bigger problems with its “smarts” → meaning that GPT would not follow the rules we very explicitly told it to follow, or would do things we explicitly forbid it from doing. GPT4 proved to be better at following rules than GPT3.5, but even GPT4 would keep doing some mistakes over and over and forgetting about specific rules (even though there was more than enough context). The “fixing” step did help with this: we would repeat the rules there and GPT would pick up more of them, but often still not all of them.

Handling JSON as a response 📋

As mentioned earlier in this article, in all our interactions with GPT, we always ask it to return the response as JSON, for which we specify the schema and give some examples.

However, GPT still doesn’t always follow that rule, and will sometimes add some text around the JSON, or will make a mistake in formatting JSON.

The way we handled this is with two simple fixes:

  1. Upon receiving JSON, we would remove all the characters from the start until we hit {, and also all chars from the end until we hit }. Simple heuristic, but it works very well for removing redundant text around the JSON in practice since GPT will normally not have any { or } in that text.
  2. If we fail to parse JSON, we send it again for repairs, to GPT. We include the previous prompt and its last answer (that contains invalid JSON) and add instructions to fix it + JSON parse errors we got. We repeat this a couple of times until it gets it right (or until we give up).

In practice, these two methods took care of invalid JSON in 99% of the cases for us.

NOTE: While we were implementing our code agent, OpenAI released new functionality for GPT, “functions”, which is basically a mechanism to have GPT respond with a structured JSON, following the schema of your description. So it would likely make more sense to do this with “functions”, but we already had this working well so we just stuck with it.

Handling interruptions in the service 🚧

We were calling OpenAI API directly, so we noticed quickly that often it would return 503 - service unavailable - especially during peak hours (e.g. 3 pm CET).

Therefore, it is recommended to have some kind of retry mechanism, ideally with exponential backoff, that makes your code agent redundant to such random interruptions in the service, and also to potential rate limiting. We went with the retry mechanism with exponential backoff and it worked great.

Temperature 🌡️

Temperature determines how creative GPT is, but the more creative it gets, the less “stable” it is. It hallucinates more and also has a harder time following rules. +

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

· 23 min read
Martin Sosic

We created GPT Web App Generator, which lets you shortly describe the web app you would like to create, and in a matter of minutes, a full-stack codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available to download and run locally!

We started this as an experiment, to see how well we could use GPT to generate full-stack web apps in Wasp, the open-source JS web app framework that we are developing. Since we launched, we had more than 3000 apps generated in just a couple of days!

1. Describe your app 2. Pick the color 3. Generate your app 🚀

Check out this blog post to see GPT Web App Generator in action, including a one-minute demo video, few example apps, and learn a bit more about our plans for the future. Or, try it out yourself at https://magic-app-generator.wasp-lang.dev/ !

In this blog post, we are going to explore the technical side of creating the GPT Web App Generator: techniques we used, how we engineered our prompts, challenges we encountered, and choices we made! (Note from here on we will just refer to it as the “Generator”, or “code agent” when talking about the backend)

Also, all the code behind the Generator is open source: web app, GPT code agent.

How well does it work 🤔?

First, let’s quickly explain what we ended up with and how it performs.

Input into our Generator is the app name, app description (free form text), and a couple of simple options such as primary app color, temperature, auth method, and GPT model to use.

Input for generating a Todo app

As an output, Generator spits out the whole JS codebase of a working full-stack web app: frontend, backend, and database. Frontend is React + Tailwind, the backend is NodeJS with Express, and for working with the database we used Prisma. This is all connected together with the Wasp framework.

You can see an example of generated codebase here: https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f .

Result of generating a Todo app

Generator does its best to produce code that works out of the box → you can download it to your machine and run it. For simpler apps, such as TodoApp or MyPlants, it often generates code with no mistakes, and you can run them out of the box.

What generated TodoApp looks like

For a bit more complex apps, like a blog with posts and comments, it still generates a reasonable codebase but there are some mistakes to be expected here and there. For even more complex apps, it usually doesn’t follow up completely, but stops at some level of complexity and fills in the rest with TODOs or omits functionality, so it is kind of like a simplified model of what was asked for. Overall, it is optimized for producing CRUD business web apps.

This makes it a great tool for kick-starting your next web app project with a solid prototype, or to even generate working, simple apps on the fly!

How does it work ⚙️?

When we set out to build the Generator, we gave ourselves the following goals:

  • we must be able to build it in a couple of weeks
  • it has to be relatively easy to maintain in the future
  • it needs to generate the app quickly and cheaply (a couple of minutes, < $1)
  • generated apps should have as few mistakes as possible

Therefore, to keep it simple, we don’t do any LLM-level engineering or fine-tuning, instead, we just use OpenAI API (specifically GPT3.5 and GPT4) to generate different parts of the app while giving it the right context at every moment (pieces of docs, examples, guidelines, …). To ensure the coherence and quality of the generated app, we don’t give our code agent too much freedom but instead heavily guide it, step by step, through generating the app.

As step zero, we generate some code files deterministically, without GPT, just based on the options that the user chose (primary color, auth method): those include some config files for the project, some basic global CSS, and some auth logic. You can see this logic here (we call those “skeleton” files): code on Github .

Then, the code agent takes over!

The code agent does its work in 3 main phases:

  1. Planning 📝
  2. Generating 🏭
  3. Fixing 🔧

Since GPT4 is quite slower and significantly more expensive than GPT3.5 (also has a lower rate limit regarding the number of tokens per minute, and also the number of requests per minute), we use GPT4 only for the planning, since that is the crucial step, and then after that, we use GPT3.5 for the rest.

As for cost per app 💸: one app typically consumes from 25k to 60k tokens, which comes to about $0.1 to $0.2 per app, when we use a mix of GPT4 and GPT3.5. If we run it just with GPT4, then the cost is 10x, which is from $1 to $2.

🎶 Intermezzo: short explanation of OpenAI Chat Completions API

OpenAI API offers different services, but we used only one of them: “chat completions”.

API itself is actually very simple: you send over a conversation, and you get a response from the GPT.

The conversation is just a list of messages, where each message has content and a role, where the role specifies who “said” that content → was it “user” (you), or “assistant” (GPT).

The important thing to note is that there is no concept of state/memory: every API call is completely standalone, and the only thing that GPT knows about is the conversation you provide it with at that moment!

If you are wondering how ChatGPT (the web app that uses GPT in the background) works with no memory → well, each time you write a message, the whole conversation so far is resent again! There are some additional smart mechanisms in play here, but that is really it at its core.

Official guide, official API reference.

Step #1: Planning 📝

A Wasp app consists of Entities (Prisma data models), Operations (NodeJS Queries and Actions), and Pages (React).

Once given an app description and title, the code agent first generates a Plan: it is a list of Entities, Operations (Queries and Actions), and Pages that comprise the app. So kind of like an initial draft of the app. It doesn’t generate the code yet → instead, it comes up with their names and some other details, including a short description of what they should behave like.

This is done via a single API request toward GPT, where the prompt consists of the following:

  • Short info about the Wasp framework + an example of some Wasp code.
  • We explain that we want to generate the Plan, explain what it is, and how it is represented as JSON, by describing its schema.
  • We provide some examples of the Plan, represented as JSON.
  • Some rules and guidelines we want it to follow (e.g. “plan should have at least 1 page”, “make sure to generate a User entity”).
  • Instructions to return the Plan only as a valid JSON response, and no other text.
  • App name and description (as provided by the user).

You can see how we generate such a prompt in the code here.

Also, here is an actual instance of this prompt for a TodoApp.
Wasp is a full-stack web app framework that uses React (for client), NodeJS and Prisma (for server).
High-level of the app is described in main.wasp file (which is written in special Wasp DSL), details in JS/JSX files.
Wasp DSL (used in main.wasp) reminds a bit of JSON, and doesn't use single quotes for strings, only double quotes. Examples will follow.

Important Wasp features:
- Routes and Pages: client side, Pages are written in React.
- Queries and Actions: RPC, called from client, execute on server (nodejs).
Queries are for fetching and should not do any mutations, Actions are for mutations.
- Entities: central data models, defined via PSL (Prisma schema language), manipulated via Prisma.
Typical flow: Routes point to Pages, Pages call Queries and Actions, Queries and Actions work with Entities.

Example main.wasp (comments are explanation for you):

```wasp
app todoApp {
wasp: { version: "^0.11.1" },
title: "ToDo App",
auth: {
userEntity: User,
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login"
},
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
db: {
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx"
}

route DashboardRoute { path: "/", to: Dashboard }
page DashboardPage {
authRequired: true,
component: import Dashboard from "@client/pages/Dashboard.jsx"
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
psl=}

entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

query getUser {
fn: import { getUser } from "@server/queries.js",
entities: [User] // Entities that this query operates on.
}

query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}

action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}

action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
```

We are looking for a plan to build a new Wasp app (description at the end of prompt).

Instructions you must follow while generating plan:
- App uses username and password authentication.
- App MUST have a 'User' entity, with following fields required:
- `id Int @id @default(autoincrement())`
- `username String @unique`
- `password String`
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
- One of the pages in the app must have a route path "/".
- Don't generate the Login or Signup pages and routes under any circumstances. They are already generated.

Plan is represented as JSON with the following schema:

{
"entities": [{ "entityName": string, "entityBodyPsl": string }],
"actions": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"queries": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"pages": [{ "pageName": string, "componentPath": string, "routeName": string, "routePath": string, "pageDesc": string }]
}

Here is an example of a plan (a bit simplified, as we didn't list all of the entities/actions/queries/pages):

{
"entities": [{
"entityName": "User",
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]"
}],
"actions": [{
"opName": "createTask",
"opFnPath": "@server/actions.js",
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
}],
"queries": [{
"opName": "getTask",
"opFnPath": "@server/queries.js",
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
}],
"pages": [{
"pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
}]
}

We will later use this plan to write main.wasp file and all the other parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Also, mention in the descriptions of actions/queries which entities they work with,
and in descriptions of pages mention which actions/queries they use.

Typically, plan will have AT LEAST one query, at least one action, at least one page, and at
least two entities. It will very likely have more than one of each, though.

DO NOT create actions for login and logout under any circumstances. They are already included in Wasp.

Note that we are using SQLite as a database for Prisma, so don't use scalar arrays in PSL, like `String[]`,
as those are not supported in SQLite. You can of course normally use arrays of other models, like `Task[]`.

Please, respond ONLY with a valid JSON that is a plan.
There should be no other text in the response.

==== APP DESCRIPTION: ====

App name: TodoApp
A simple todo app with one main page that lists all the tasks. User can create new tasks by providing their description, toggle existing ones, or edit their description. User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.

GPT then responds with a JSON (hopefully), that we parse, and we have ourselves a Plan! We will use this Plan in the following steps, to drive our generation of other parts of the app. Note that GPT sometimes adds text to the JSON response or returns invalid JSON, so we built in some simple approaches to overcome these issues, which we explain in detail later.

🎶 Intermezzo: Common prompt design

The prompt design we just described above for generating a Plan is actually very similar for other steps (e.g. the Generation and Fixing steps along with their respective sub-steps), so let’s cover those commonalities.

All of the prompts we use more or less adhere to the same basic structure:

  • General context
    • Short info about what Wasp framework is.
    • Doc snippets (with code examples if needed) about whatever we are generating right now (e.g. examples of NodeJS code, or examples of React code).
  • Project context: stuff we generated in the previous steps that is relevant to the current step.
  • Instructions on what we want to generate right now + JSON schema for it + example of such JSON response.
  • Rules and guidelines: this is a good place to warn it about common mistakes it makes, or give it some additional advice, and emphasize what needs to happen and what must not happen.
  • Instructions to respond only with a valid JSON, and no other text.
  • Original user prompt: app name and description (as provided by the user).

We put the original user prompt at the end because then we can tell GPT in the system message after it sees the start of the original user prompt (we have a special header for it), that it needs to treat everything after it as an app description and not as instructions on what to do → this way we attempt to defend from the potential prompt injection.

Step #2: Generating 🏭

After producing the Plan, Generator goes step by step through the Plan and asks GPT to generate each web app piece, while providing it with docs, examples, and guidelines. Each time a web app piece is generated, Generator fits it into the whole app. This is where most of our work comes in: equipping GPT with the right information at the right moment.

In our case, we do it for all the Operations in the Plan (Actions and Queries: NodeJs code), and also for all the Pages in the Plan (React code), with one prompt for each. So if we have 2 queries, 3 actions, and 2 pages, that will be 2+3+2 = 7 GPT prompts/requests. Prompts are designed as explained previously.

Code on Github: generating an Operation, generating a Page.

When generating Operations, we provide GPT with the info about the previously generated Entities, while when generating Pages, we provide GPT with the info about previously generated Entities and Operations.

Step #3: Fixing 🔧

Finally, the Generator tries its best to fix any mistakes that GPT might have introduced previously. GPT loves fixing stuff it previously generated → if you first ask it to generate some code, and then just tell it to fix it, it will often improve it!

To enhance this process further, we don’t just ask it to fix previous code, but also provide it with instructions on what to keep an eye out for, like common types of mistakes that we noticed it often does, and also point it to any specific mistakes we were able to detect on our own.

Regarding detecting mistakes to report to GPT, ideally, you would have a full REPL going on → that means running the generated code through an interpreter/compiler, then sending it for repairs, and so on until all is fixed.

In our case, running the whole project through the TypeScript compiler was not feasible for us with the time limits we put on ourselves, but we used some simpler static analysis tools like Wasp’s compiler (for the .wasp file) and prisma format for Prisma model schemas, and sent those to GPT to fix them. We also wrote some simple heuristics of our own that are able to detect some of the common mistakes.

Our code (& prompt) for fixing a Page.

Our code (& prompt) for fixing Operations.

In the prompt, we would usually repeat the same guidelines we provided previously in the Generation step, while also adding a couple of additional pointers to common mistakes, and that usually helps, it fixes stuff it missed before. But, often not everything, instead something will still get through. Some things we just couldn’t get it to fix consistently, for example, Wasp-specific JS imports, no matter how much we emphasized what it needed to do with them, it would just keep messing them up. Even GPT4 wasn’t perfect in this situation. For such situations, when possible, we ended up writing our own heuristics that would fix those mistakes (fixing JS imports).

Things we tried/learned

Explanations 💬

We tried telling GPT to explain what it did while fixing mistakes: which mistakes it will fix, and which mistakes it fixed, since we read that that can help, but we didn’t see visible improvement in its performance.

Testing 🧪

Testing the performance of your code agent is hard.

In our case, it takes a couple of minutes for our code agent to generate a new app, and you need to run tests directly with the OpenAI API. Also, since results are non-deterministic, it can be pretty hard to say if output was affected by the changes you did or not.

Finally, evaluating the output itself can be hard (especially in our case when it is a whole full-stack web app).

Ideally, we would have set up a system where we can run only parts of the whole generation process, and we could automatically run a specific part a number of times for each of different sets of parameters (which would include different prompts, but also parameters like type of model (gpt4 vs gpt3.5), temperature and similar), in order to compare performance for each of those parameter sets.

Evaluation performance would also ideally be automated, e.g. we would count the mistakes during compilation and/or evaluate the quality of app design → but this is also quite hard.

We, unfortunately, didn’t have time to set up such a system, so we were mostly doing testing manually, which is quite subjective and vulnerable to randomness, and is effective only for changes that have quite a big impact, while you can’t really detect those that are minor optimizations.

Context vs smarts 🧠

When we started working on the Generator, we thought the size of GPT’s context would be the main issue. However, we didn’t have any issues with context at the end → most of what we wanted to specify would fit into 2k to max 4k tokens, while GPT3.5 has context up to 16k!

Instead, we had bigger problems with its “smarts” → meaning that GPT would not follow the rules we very explicitly told it to follow, or would do things we explicitly forbid it from doing. GPT4 proved to be better at following rules than GPT3.5, but even GPT4 would keep doing some mistakes over and over and forgetting about specific rules (even though there was more than enough context). The “fixing” step did help with this: we would repeat the rules there and GPT would pick up more of them, but often still not all of them.

Handling JSON as a response 📋

As mentioned earlier in this article, in all our interactions with GPT, we always ask it to return the response as JSON, for which we specify the schema and give some examples.

However, GPT still doesn’t always follow that rule, and will sometimes add some text around the JSON, or will make a mistake in formatting JSON.

The way we handled this is with two simple fixes:

  1. Upon receiving JSON, we would remove all the characters from the start until we hit {, and also all chars from the end until we hit }. Simple heuristic, but it works very well for removing redundant text around the JSON in practice since GPT will normally not have any { or } in that text.
  2. If we fail to parse JSON, we send it again for repairs, to GPT. We include the previous prompt and its last answer (that contains invalid JSON) and add instructions to fix it + JSON parse errors we got. We repeat this a couple of times until it gets it right (or until we give up).

In practice, these two methods took care of invalid JSON in 99% of the cases for us.

NOTE: While we were implementing our code agent, OpenAI released new functionality for GPT, “functions”, which is basically a mechanism to have GPT respond with a structured JSON, following the schema of your description. So it would likely make more sense to do this with “functions”, but we already had this working well so we just stuck with it.

Handling interruptions in the service 🚧

We were calling OpenAI API directly, so we noticed quickly that often it would return 503 - service unavailable - especially during peak hours (e.g. 3 pm CET).

Therefore, it is recommended to have some kind of retry mechanism, ideally with exponential backoff, that makes your code agent redundant to such random interruptions in the service, and also to potential rate limiting. We went with the retry mechanism with exponential backoff and it worked great.

Temperature 🌡️

Temperature determines how creative GPT is, but the more creative it gets, the less “stable” it is. It hallucinates more and also has a harder time following rules. A temperature is a number from 0 to 2, with a default value of 1.

We experimented with different values and found the following:

  • ≥ 1.5 would every so and so start giving quite silly results with random strings in it.
  • ≥ 1.0, < 1.5 was okish but was introducing a bit too many mistakes.
  • ≥ 0.7, < 1.0 was optimal → creative enough, while still not having many mistakes.
  • ≤ 0.7 seemed to perform similarly to a bit higher values, but with a bit less creativity maybe.

That said, I don’t think we tested values below 0.7 enough, and that is something we could certainly work on more.

We ended up using 0.7 as our default value, except for prompts that do fixing, for those we used a lower value of 0.5 because it seemed like GPT was changing stuff too much while fixing at 0.7 (being too creative). Our logic was: let it be creative when writing the first version of the code, then have it be a bit more conventional while fixing it. Again, we haven’t tested all this enough, so this is certainly something I would like us to explore more.

Future 🔮

While we ended up being impressed with the performance of what we managed to build in such a short time, we were also left wanting to try so many different ideas on how to improve it further. There are many avenues left to be explored in this ecosystem that is developing so rapidly, that it is hard to reach the point where you feel like you explored all the options and found the optimal solution.

Some of the ideas that would be exciting to try in the future:

  1. We put quite a few limitations regarding the code that our code agent generates, to make sure it works well enough: we don’t allow it to create helper files, to include npm dependencies, no TypeScript, no advanced Wasp features, … . We would love to lift the limitations, therefore allowing the creation of more complex and powerful apps.

  2. Instead of our code agent doing everything in one shot, we could allow the user to interact with it after the first version of the app is generated: to provide additional prompts, for example, to fix something, to add some feature to the app, to do something differently, …. The hardest thing here would be figuring out which context to provide to the GPT at which moment and designing the experience appropriately, but I am certain it is doable, and it would take the Generator to the next level of usability. Another option is to allow intervention in between initial generation steps → for example, after the plan is generated, to allow the user to adjust it by providing additional instructions to the GPT.

  3. Find an open-source LLM that fits the purpose and fine-tune / pre-train it for our purpose. If we could teach it more about Wasp and the technologies we use, so we don’t have to include it in every prompt, we could save quite some context + have the LLM be more focused on the rules and guidelines we are specifying in the prompt. We could also host it ourselves and have more control over the costs and rate limits.

  4. Take a different approach to the code agent: let it be more free. Instead of guiding it so carefully, we could teach it about all the different things it is allowed to ask for (ask for docs, ask for examples, ask to generate a certain piece of the app, ask to see a certain already generated piece of the app, …) and would let it guide itself more freely. It could constantly generate a plan, execute it, update the plan, and so on until it reaches the state of equilibrium. This approach potentially promises more flexibility and would likely be able to generate apps of greater complexity, but it also requires quite more tokens and a powerful LLM to drive it → I believe this approach will become more feasible as LLMs become more capable.

Support us! ⭐️

If you wish to express your support for what we are doing, consider giving us a star on Github! Everything we do at Wasp is open source, and your support motivates us and helps us to keep making web app development easier and with less boilerplate.

Also, if you have any ideas on how we could improve our code agent, or maybe we can help you somehow -> feel free to join our Discord server and let's chat!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/08/01/smol-ai-vs-wasp-ai.html b/blog/2023/08/01/smol-ai-vs-wasp-ai.html index 68d3243632..2bd52f9fe3 100644 --- a/blog/2023/08/01/smol-ai-vs-wasp-ai.html +++ b/blog/2023/08/01/smol-ai-vs-wasp-ai.html @@ -19,19 +19,19 @@ - - + +
-

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

· 27 min read
Vinny

TL;DR

AI-assisted coding tools are on the rise. In this article, we take a deep dive into two tools that use similar techniques, but are intended for different outcomes.

Smol AI’s “Smol-Developer” gained a lot of notoriety very quickly by being one of the first such tools on the scene. It is a simple set of python scripts that allow a user to build prototype apps using natural language in an iterative approach.

Wasp’s “GPT Web App Generator” is more of a newcomer and focuses on building more complex full-stack React + NodeJS web app prototypes through a simple prompt and fancy UI.

When comparing the two, Smol-Developer’s strength is its versatility. If you want to spend time tinkering and tweaking, you can do a lot to your own prompting, and even the code, in order to get decent results on a broad range of apps.

On the other hand, Wasp AI shines by being specific. Because it’s only built for generating full-stack React/NodeJS/Prisma/Tailwind codebases, it does the tweaking and advanced prompting for you, and thus it performs much better in generating higher quality content with less effort for a specific use case.

Will either of these tools completely replace Junior Developers in their current form? Of course not. But they do allow for rapid prototyping and testing of novel ideas.

Read on to learn more about how they work, which tool is right for the job at hand, and how you can use them in your current workflow.

Intro

The age of AI-assisted coding tools is fully upon us. GitHub’s Copilot might be the go-to professional solution, but since its release numerous open-source solutions have popped up.

Most of these newer solutions tend towards functioning as an AI Agent, going beyond just suggesting the next logical pieces of code within your current file, they aim to create simple prototypes of entire apps. Some are focused more on scaffolding entire app prototypes from an initial prompt, while others function as interactive assistants, helping you modify and improve existing codebases.

Either way, they’re often being described as “AI Junior Developers”, because they can take a product requirement (i.e. “prompt”) and build a pretty good — but far from perfect — first iteration, saving developers a lot of time.

This article is going to focus on two tools that aim to build somewhat complex working prototypes from a single prompt: Smol AI and Wasp AI. We’ll test them out by running the same prompts through each and seeing what we get.

By the end of it, you’ll have a pretty good understanding of how they work, their advantages and disadvantages, and what kind of tasks they’re best suited for.

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built-in compiler and AI-assisted features that lets you build your app super quickly.

We’re working hard to help you build performant web apps as easily as possible — including creating content like this, which is released weekly!

We would be super grateful if you could help us out by starring our repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

please please please

…even Ron would star Wasp on GitHub 🤩

The Tools

Smol-Developer

Smol AI (described as a platform for “model distillation and AI developer agents”) actually has a few open-source tools on offer, but Smol-Developer is the one we’ll be taking a look at. It was initially released by Swyx on May 11th and already has over 10k GitHub stars!

It aims to be a generalist, prompt-based coding assistant run from the command line. The developer’s job becomes a process of iterative prompting, testing, and re-prompting in order to get the optimal output. It is not limited to any language or type of app it can create, although simple apps tend to work best.

Check out this tweet thread above to get a better understanding: https://twitter.com/swyx/status/1657892220492738560

Running from the command line, Smol AI is essentially a chain of calls to the OpenAI chat completions (i.e. “ChatGpt”) endpoint via a python script that:

  1. takes an initial user-generated prompt
  2. creates a plan based on internal prompts* for executing the app with:
    1. the structure of the entire app
    2. each file and its exported variables to be generated
    3. function names
  3. generates file paths based on the plan
  4. loops through file paths and generates code for each file based on plan and prompt

The generated output can then be evaluated by the developer and the prompt can be iterated on to account for any errors or bugs found during runtime.

Smol-Developer quickly gained notoriety by being one of the first of such tools on the scene, in addition to Swyx’s prominence within it. So if you’re curious to see what’s being built with it, just check out some of the numerous YouTube videos on it.

One of my personal favorites is AI Jason’s exposé and commentary. He gives a concise explanation, shows you some great tips on how to use Smol-Developer effectively, and as a Product Designer/Manager he gives an interesting perspective on its benefits:

*Curious to see what the internal system prompt looks like?
You are a top tier AI developer who is trying to write a program that will generate code for the user based on their intent.

Do not leave any todos, fully implement every feature requested.

When writing code, add comments to explain what you intend to do and why it aligns with the program plan and specific instructions from the original prompt.

In response to the user's prompt, write a plan.

In this plan, please name and briefly describe the structure of the app we will generate, including, for each file we are generating, what variables they export, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names.

Respond only with plans following the above schema.

the app prompt is: {prompt}

Wasp’s GPT Web App Generator

In contrast to Smol-Developer, Wasp’s AI tool, GPT Web App Generator, is currently an open-source web app (yes, it’s a web app that makes web apps). Since it’s release on the 12th of July, there have been over 6,500 apps generated with over 300 apps being generated each day!

Here’s a quick 1 minute video showcasing how GPT Web App Generator works:

So to give a bit of background, Wasp is actually a full-stack web app framework built around a compiler and config file. Using this approach, Wasp simplifies the web app creation process by handling boilerplate code for you, taking the core app logic written by the developer and connecting the entire stack together from frontend to backend, and database management.

It currently works with React, NodeJS, Tanstack-Query, and Prisma, taking care of features like Auth, Routing, Cron Jobs, Fullstack Typesafety, and Caching. This allows developers to focus more on the fun stuff, like the app’s features, instead of spending time on boring configurations.

Because Wasp uses a compiler and config file to generate the app from, this makes it surprisingly well suited for guiding LLMs like ChatGPT towards creating more complex apps with it, as it essentially a plan or set of instructions for how to build the app!

Take this simple example of how you’d tell Wasp that you want username and password authentication in your app:

// main.wasp file

app RecipeApp {
title: "My Recipes",
wasp: { version: "^0.11.0" },
auth: {
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login",
userEntity: User
}
}

entity User {=psl // Data models are defined using Prisma Schema Language.
id Int @id @default(autoincrement())
username String @unique
password String
recipes Recipe[]
psl=}

Wasp’s config file is like an app outline that the compiler understands and can then use to connect and glue the app together, taking care of the boilerplate for you.

By leveraging the powers of Wasp, GPT Web App Generator works by:

  1. taking a simple user-generated prompt via the UI
  2. giving GPT a descriptive example of a Wasp app and config file via internal prompts*
  3. creating a plan that meets these requirements
  4. generating the code for each part of the app according to the plan
  5. checking each file for expected errors/hallucinations and fixing them

In the end, the user can download the codebase as a zipped file and run it locally. Simpler apps, such as TodoApp or MyPlants tend to work straight out of the box, while more complex apps need a bit of finessing to get working.

*Curious to see what the internal system prompt looks like?
Wasp is a full-stack web app framework that uses React (for client), NodeJS and Prisma (for server).
High-level of the app is described in main.wasp file (which is written in special Wasp DSL), details in JS/JSX files.
Wasp DSL (used in main.wasp) reminds a bit of JSON, and doesn't use single quotes for strings, only double quotes. Examples will follow.

Important Wasp features:
- Routes and Pages: client side, Pages are written in React.
- Queries and Actions: RPC, called from client, execute on server (nodejs).
Queries are for fetching and should not do any mutations, Actions are for mutations.
- Entities: central data models, defined via PSL (Prisma schema language), manipulated via Prisma.
Typical flow: Routes point to Pages, Pages call Queries and Actions, Queries and Actions work with Entities.

Example main.wasp (comments are explanation for you):

```wasp
app todoApp {
wasp: { version: "^0.11.1" },
title: "ToDo App",
auth: {
userEntity: User,
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login"
},
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
db: {
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx"
}

route DashboardRoute { path: "/", to: Dashboard }
page DashboardPage {
authRequired: true,
component: import Dashboard from "@client/pages/Dashboard.jsx"
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
psl=}

entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

query getUser {
fn: import { getUser } from "@server/queries.js",
entities: [User] // Entities that this query operates on.
}

query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}

action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}

action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
```

We are looking for a plan to build a new Wasp app (description at the end of prompt).

Instructions you must follow while generating plan:
- App uses username and password authentication.
- App MUST have a 'User' entity, with following fields required:
- `id Int @id @default(autoincrement())`
- `username String @unique`
- `password String`
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
- One of the pages in the app must have a route path "/".
- Don't generate the Login or Signup pages and routes under any circumstances. They are already generated.

Plan is represented as JSON with the following schema:

{
"entities": [{ "entityName": string, "entityBodyPsl": string }],
"actions": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"queries": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"pages": [{ "pageName": string, "componentPath": string, "routeName": string, "routePath": string, "pageDesc": string }]
}

Here is an example of a plan (a bit simplified, as we didn't list all of the entities/actions/queries/pages):

{
"entities": [{
"entityName": "User",
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]"
}],
"actions": [{
"opName": "createTask",
"opFnPath": "@server/actions.js",
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
}],
"queries": [{
"opName": "getTask",
"opFnPath": "@server/queries.js",
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
}],
"pages": [{
"pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
}]
}

We will later use this plan to write main.wasp file and all the other parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Also, mention in the descriptions of actions/queries which entities they work with,
and in descriptions of pages mention which actions/queries they use.

Typically, plan will have AT LEAST one query, at least one action, at least one page, and at
least two entities. It will very likely have more than one of each, though.

DO NOT create actions for login and logout under any circumstances. They are already included in Wasp.

Note that we are using SQLite as a database for Prisma, so don't use scalar arrays in PSL, like `String[]`,
as those are not supported in SQLite. You can of course normally use arrays of other models, like `Task[]`.

Please, respond ONLY with a valid JSON that is a plan.
There should be no other text in the response.

==== APP DESCRIPTION: ====

App name: TodoApp
A simple todo app with one main page that lists all the tasks. User can create new tasks by providing their description, toggle existing ones, or edit their description. User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.

Comparison Test

Prompt 1: PONG Game

To get a sense for how each coding agent performed, I tried out two different prompts on both Smol-Developer and Wasp’s GPT Web App Generator with only slight modifications to the prompts to fit the requirements of each tool.

The first prompt was the default prompt that comes hardcoded into Smol-Developer’s [main.py](http://main.py) script:

a simple JavaScript/HTML/CSS/Canvas app that is a one player game of PONG. The left paddle is controlled by the player, following where the mouse goes. The right paddle is controlled by a simple AI algorithm, which slowly moves the paddle toward the ball at every frame, with some probability of error. Make the canvas a 400 x 400 black square and center it in the app. Make the paddles 100px long, yellow and the ball small and red. Make sure to render the paddles and name them so they can controlled in javascript. Implement the collision detection and scoring as well. Every time the ball bounces off a paddle, the ball should move faster.

note

💡 For Wasp’s GPT Web App Generator, I replaced the first line with “a simple one player game of PONG” since Wasp will automatically generate a full-stack React/NodeJS app.

Both were able to create a functional PONG game out-of-the box, but only on the second try. The first try created decent PONG starters, but both had buggy game logic (e.g. computer opponent failed to hit ball, or ball would spin off into oblivion). I didn’t change the prompts at all, but just simply ran them a second time each — and that did the trick!

Smol AI’s PONG game

Wasp AI’s PONG game

For both of the generated apps, the game logic was very simple. Scores weren’t recorded, and once a game ended, you’d have to refresh the page to start a new one.

Although, while Smol-Developer only created the game logic, GPT Web App Generator created the game logic as well as the logic for authentication, creating games, and updating a game’s score, saving it all to the database (though the scoring functions weren’t being utilized initially).

To be fair, this isn’t really a surprise though as these features are baked into the design of Wasp and the Generator.

On the other hand, to get these same features for Smol-Developer, we’d have to elaborate on our prompt, giving it explicit instructions to implement them, and iterate on it a number of times before landing on an acceptable prototype.

This is what I attempted to test out with the second prompt.

Prompt 2: Blog App

Untitled

This time, for the second app test, I used a default prompt featured on the GPT Web App Generator homepage for creating a Blog app:

A blogging platform with posts and post comments. +

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

· 27 min read
Vinny

TL;DR

AI-assisted coding tools are on the rise. In this article, we take a deep dive into two tools that use similar techniques, but are intended for different outcomes.

Smol AI’s “Smol-Developer” gained a lot of notoriety very quickly by being one of the first such tools on the scene. It is a simple set of python scripts that allow a user to build prototype apps using natural language in an iterative approach.

Wasp’s “GPT Web App Generator” is more of a newcomer and focuses on building more complex full-stack React + NodeJS web app prototypes through a simple prompt and fancy UI.

When comparing the two, Smol-Developer’s strength is its versatility. If you want to spend time tinkering and tweaking, you can do a lot to your own prompting, and even the code, in order to get decent results on a broad range of apps.

On the other hand, Wasp AI shines by being specific. Because it’s only built for generating full-stack React/NodeJS/Prisma/Tailwind codebases, it does the tweaking and advanced prompting for you, and thus it performs much better in generating higher quality content with less effort for a specific use case.

Will either of these tools completely replace Junior Developers in their current form? Of course not. But they do allow for rapid prototyping and testing of novel ideas.

Read on to learn more about how they work, which tool is right for the job at hand, and how you can use them in your current workflow.

Intro

The age of AI-assisted coding tools is fully upon us. GitHub’s Copilot might be the go-to professional solution, but since its release numerous open-source solutions have popped up.

Most of these newer solutions tend towards functioning as an AI Agent, going beyond just suggesting the next logical pieces of code within your current file, they aim to create simple prototypes of entire apps. Some are focused more on scaffolding entire app prototypes from an initial prompt, while others function as interactive assistants, helping you modify and improve existing codebases.

Either way, they’re often being described as “AI Junior Developers”, because they can take a product requirement (i.e. “prompt”) and build a pretty good — but far from perfect — first iteration, saving developers a lot of time.

This article is going to focus on two tools that aim to build somewhat complex working prototypes from a single prompt: Smol AI and Wasp AI. We’ll test them out by running the same prompts through each and seeing what we get.

By the end of it, you’ll have a pretty good understanding of how they work, their advantages and disadvantages, and what kind of tasks they’re best suited for.

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built-in compiler and AI-assisted features that lets you build your app super quickly.

We’re working hard to help you build performant web apps as easily as possible — including creating content like this, which is released weekly!

We would be super grateful if you could help us out by starring our repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

please please please

…even Ron would star Wasp on GitHub 🤩

The Tools

Smol-Developer

Smol AI (described as a platform for “model distillation and AI developer agents”) actually has a few open-source tools on offer, but Smol-Developer is the one we’ll be taking a look at. It was initially released by Swyx on May 11th and already has over 10k GitHub stars!

It aims to be a generalist, prompt-based coding assistant run from the command line. The developer’s job becomes a process of iterative prompting, testing, and re-prompting in order to get the optimal output. It is not limited to any language or type of app it can create, although simple apps tend to work best.

Check out this tweet thread above to get a better understanding: https://twitter.com/swyx/status/1657892220492738560

Running from the command line, Smol AI is essentially a chain of calls to the OpenAI chat completions (i.e. “ChatGpt”) endpoint via a python script that:

  1. takes an initial user-generated prompt
  2. creates a plan based on internal prompts* for executing the app with:
    1. the structure of the entire app
    2. each file and its exported variables to be generated
    3. function names
  3. generates file paths based on the plan
  4. loops through file paths and generates code for each file based on plan and prompt

The generated output can then be evaluated by the developer and the prompt can be iterated on to account for any errors or bugs found during runtime.

Smol-Developer quickly gained notoriety by being one of the first of such tools on the scene, in addition to Swyx’s prominence within it. So if you’re curious to see what’s being built with it, just check out some of the numerous YouTube videos on it.

One of my personal favorites is AI Jason’s exposé and commentary. He gives a concise explanation, shows you some great tips on how to use Smol-Developer effectively, and as a Product Designer/Manager he gives an interesting perspective on its benefits:

*Curious to see what the internal system prompt looks like?
You are a top tier AI developer who is trying to write a program that will generate code for the user based on their intent.

Do not leave any todos, fully implement every feature requested.

When writing code, add comments to explain what you intend to do and why it aligns with the program plan and specific instructions from the original prompt.

In response to the user's prompt, write a plan.

In this plan, please name and briefly describe the structure of the app we will generate, including, for each file we are generating, what variables they export, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names.

Respond only with plans following the above schema.

the app prompt is: {prompt}

Wasp’s GPT Web App Generator

In contrast to Smol-Developer, Wasp’s AI tool, GPT Web App Generator, is currently an open-source web app (yes, it’s a web app that makes web apps). Since it’s release on the 12th of July, there have been over 6,500 apps generated with over 300 apps being generated each day!

Here’s a quick 1 minute video showcasing how GPT Web App Generator works:

So to give a bit of background, Wasp is actually a full-stack web app framework built around a compiler and config file. Using this approach, Wasp simplifies the web app creation process by handling boilerplate code for you, taking the core app logic written by the developer and connecting the entire stack together from frontend to backend, and database management.

It currently works with React, NodeJS, Tanstack-Query, and Prisma, taking care of features like Auth, Routing, Cron Jobs, Fullstack Typesafety, and Caching. This allows developers to focus more on the fun stuff, like the app’s features, instead of spending time on boring configurations.

Because Wasp uses a compiler and config file to generate the app from, this makes it surprisingly well suited for guiding LLMs like ChatGPT towards creating more complex apps with it, as it essentially a plan or set of instructions for how to build the app!

Take this simple example of how you’d tell Wasp that you want username and password authentication in your app:

// main.wasp file

app RecipeApp {
title: "My Recipes",
wasp: { version: "^0.11.0" },
auth: {
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login",
userEntity: User
}
}

entity User {=psl // Data models are defined using Prisma Schema Language.
id Int @id @default(autoincrement())
username String @unique
password String
recipes Recipe[]
psl=}

Wasp’s config file is like an app outline that the compiler understands and can then use to connect and glue the app together, taking care of the boilerplate for you.

By leveraging the powers of Wasp, GPT Web App Generator works by:

  1. taking a simple user-generated prompt via the UI
  2. giving GPT a descriptive example of a Wasp app and config file via internal prompts*
  3. creating a plan that meets these requirements
  4. generating the code for each part of the app according to the plan
  5. checking each file for expected errors/hallucinations and fixing them

In the end, the user can download the codebase as a zipped file and run it locally. Simpler apps, such as TodoApp or MyPlants tend to work straight out of the box, while more complex apps need a bit of finessing to get working.

*Curious to see what the internal system prompt looks like?
Wasp is a full-stack web app framework that uses React (for client), NodeJS and Prisma (for server).
High-level of the app is described in main.wasp file (which is written in special Wasp DSL), details in JS/JSX files.
Wasp DSL (used in main.wasp) reminds a bit of JSON, and doesn't use single quotes for strings, only double quotes. Examples will follow.

Important Wasp features:
- Routes and Pages: client side, Pages are written in React.
- Queries and Actions: RPC, called from client, execute on server (nodejs).
Queries are for fetching and should not do any mutations, Actions are for mutations.
- Entities: central data models, defined via PSL (Prisma schema language), manipulated via Prisma.
Typical flow: Routes point to Pages, Pages call Queries and Actions, Queries and Actions work with Entities.

Example main.wasp (comments are explanation for you):

```wasp
app todoApp {
wasp: { version: "^0.11.1" },
title: "ToDo App",
auth: {
userEntity: User,
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login"
},
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
db: {
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx"
}

route DashboardRoute { path: "/", to: Dashboard }
page DashboardPage {
authRequired: true,
component: import Dashboard from "@client/pages/Dashboard.jsx"
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
psl=}

entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

query getUser {
fn: import { getUser } from "@server/queries.js",
entities: [User] // Entities that this query operates on.
}

query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}

action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}

action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
```

We are looking for a plan to build a new Wasp app (description at the end of prompt).

Instructions you must follow while generating plan:
- App uses username and password authentication.
- App MUST have a 'User' entity, with following fields required:
- `id Int @id @default(autoincrement())`
- `username String @unique`
- `password String`
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
- One of the pages in the app must have a route path "/".
- Don't generate the Login or Signup pages and routes under any circumstances. They are already generated.

Plan is represented as JSON with the following schema:

{
"entities": [{ "entityName": string, "entityBodyPsl": string }],
"actions": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"queries": [{ "opName": string, "opFnPath": string, "opDesc": string }],
"pages": [{ "pageName": string, "componentPath": string, "routeName": string, "routePath": string, "pageDesc": string }]
}

Here is an example of a plan (a bit simplified, as we didn't list all of the entities/actions/queries/pages):

{
"entities": [{
"entityName": "User",
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]"
}],
"actions": [{
"opName": "createTask",
"opFnPath": "@server/actions.js",
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
}],
"queries": [{
"opName": "getTask",
"opFnPath": "@server/queries.js",
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
}],
"pages": [{
"pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
}]
}

We will later use this plan to write main.wasp file and all the other parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Also, mention in the descriptions of actions/queries which entities they work with,
and in descriptions of pages mention which actions/queries they use.

Typically, plan will have AT LEAST one query, at least one action, at least one page, and at
least two entities. It will very likely have more than one of each, though.

DO NOT create actions for login and logout under any circumstances. They are already included in Wasp.

Note that we are using SQLite as a database for Prisma, so don't use scalar arrays in PSL, like `String[]`,
as those are not supported in SQLite. You can of course normally use arrays of other models, like `Task[]`.

Please, respond ONLY with a valid JSON that is a plan.
There should be no other text in the response.

==== APP DESCRIPTION: ====

App name: TodoApp
A simple todo app with one main page that lists all the tasks. User can create new tasks by providing their description, toggle existing ones, or edit their description. User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.

Comparison Test

Prompt 1: PONG Game

To get a sense for how each coding agent performed, I tried out two different prompts on both Smol-Developer and Wasp’s GPT Web App Generator with only slight modifications to the prompts to fit the requirements of each tool.

The first prompt was the default prompt that comes hardcoded into Smol-Developer’s [main.py](http://main.py) script:

a simple JavaScript/HTML/CSS/Canvas app that is a one player game of PONG. The left paddle is controlled by the player, following where the mouse goes. The right paddle is controlled by a simple AI algorithm, which slowly moves the paddle toward the ball at every frame, with some probability of error. Make the canvas a 400 x 400 black square and center it in the app. Make the paddles 100px long, yellow and the ball small and red. Make sure to render the paddles and name them so they can controlled in javascript. Implement the collision detection and scoring as well. Every time the ball bounces off a paddle, the ball should move faster.

note

💡 For Wasp’s GPT Web App Generator, I replaced the first line with “a simple one player game of PONG” since Wasp will automatically generate a full-stack React/NodeJS app.

Both were able to create a functional PONG game out-of-the box, but only on the second try. The first try created decent PONG starters, but both had buggy game logic (e.g. computer opponent failed to hit ball, or ball would spin off into oblivion). I didn’t change the prompts at all, but just simply ran them a second time each — and that did the trick!

Smol AI’s PONG game

Wasp AI’s PONG game

For both of the generated apps, the game logic was very simple. Scores weren’t recorded, and once a game ended, you’d have to refresh the page to start a new one.

Although, while Smol-Developer only created the game logic, GPT Web App Generator created the game logic as well as the logic for authentication, creating games, and updating a game’s score, saving it all to the database (though the scoring functions weren’t being utilized initially).

To be fair, this isn’t really a surprise though as these features are baked into the design of Wasp and the Generator.

On the other hand, to get these same features for Smol-Developer, we’d have to elaborate on our prompt, giving it explicit instructions to implement them, and iterate on it a number of times before landing on an acceptable prototype.

This is what I attempted to test out with the second prompt.

Prompt 2: Blog App

Untitled

This time, for the second app test, I used a default prompt featured on the GPT Web App Generator homepage for creating a Blog app:

A blogging platform with posts and post comments. User owns posts and comments and they are saved in the database. Everybody can see all posts, but only the owner can edit or delete them. Everybody can see all the comments. App has four pages:

  1. "Home" page lists all posts (their titles and authors) and is accessible by anybody. If you click on a post, you are taken to the "View post" page. It also has a 'New post' button, that only logged in users can see, and that takes you to the "New post" page.
  2. "New post" page is accessible only by the logged in users. It has a form for creating a new post (title, content).
  3. "Edit post" page is accessible only by the post owner. It has a form for editing the post with the id specified in the url.
  4. "View post" page is accessible by anybody and it shows the details of the post with the id specified in the url: its title, author, content and comments. It also has a form for creating a new comment, that is accessible only by the logged in users.
note

💡 For the Smol-Developer prompt, I added the lines: “The app consists of a React client and a NodeJS server. Posts are saved in an sqlite database using Prisma ORM.”

As this was a suggested prompt on the GPT Web App Generator page, let’s start with the Wasp app result first.

After downloading the generated codebase and running the app, I ran into an error Failed to resolve import "./ext-src/ViewPost.jsx" from "src/router.jsx". Does the file exist?

One quick look at the main.wasp file revealed that the Generator gave the wrong path to the ViewPost page, although it did get all the other Page paths correct (highlighted in yellow above).

Once that path was corrected, a working app popped up at localhost:3000. Nice!

The video above was my first time trying out the app, and as you can see, most of the functionality is there and working correctly — Authentication and Authorization, and basic CRUD operations. Pretty amazing!

There were still a couple of errors that prevented the app from being fully functional out-of-the-box, but they were easy to fix:

  1. Blog posts on the homepage did not have a link in order to redirect to the their specific post page — fixable by just wrapping them in <Link to={/post/${post.id}}>
  2. The client was passing the postId as a String instead of an Int to the getPost endpoint — fixable by wrapping the argument in parseInt(postId) to convert strings to integers

And with those simple fixes we got a fully functioning, full-stack blog app with authentication, database, and simple tailwind css styling! The best part was that all this took about ~5 minutes from start to finish. Sweet :)

note

🧑‍💻 The Generator saves all the apps it creates along with a sharable link, so if you want to check out the original generated Blog app code (before fixes) from above, click here: https://magic-app-generator.wasp-lang.dev/result/a3a76887-952b-4774-a773-42209c4bffa8

The Smol-Developer result was also very impressive, with a solid ExpressJS server and a lot of React client pages, but there were too many complicated errors that prevented me from getting the app started, including but not limited to:

  1. No build tools or configuration files
  2. The server was importing database models that didn’t exist
  3. The server was importing but not utilizing Prisma as the ORM to communicate with the DB
  4. Client had Auth logic, but was not utilizing it to protect pages/routes

Untitled

Because there were too many fundamental issues with the app, I went ahead and added some more lines to the bottom of the prompt:

Scaffold the app to be able to use Vite as the client's build tool. Include a package.json file >with the dependencies and scripts for running the client and server.

This second attempt produced some of the changes I was looking for, like package.json files and Vite config files to bootstrap the React app, but it still failed to include:

  1. An index.html file
  2. Package.json files with the correct dependencies being imported from within the client and server
  3. A prisma.schema file
  4. A css file (although it did include classNames in the jsx code)

On the other hand, the server code, albeit much sparser this time, did at least import and use Prisma correctly.

So I went ahead for a third attempt and modified and added the following lines to the bottom of the prompt:

Scaffold the app to be able to use Vite as the client's build tool.

Make sure to include the following:

  1. package.json files for both the server and client. Make sure that these files include the >dependencies being imported in the respective apps.
  2. an index.html file in the client's public folder, so that Vite can build the app.
  3. a prisma.schema file with the models and their fields. Make sure these are the same models >being used app-wide.
  4. a css file with styles that match the classNames used in the app.

With these additions to the prompt, the third iteration of the app did in fact include them! Well, most of them, but unfortunately not all of them. Now I was getting the css and package.json files, but no vite config file was created this time, even though the instructions for using “Vite as the client’s build tool” produced one previously.

Besides that, no auth logic was implemented, imports were out place or missing, and an index.jsx file was also nowhere to be found, so I decided to stop there.

I’m sure I could have iterated on the prompt enough times until I got closer to a working app, but at ~$0.80-$1.20 a generation, I didn’t feel like racking up more of an OpenAI bill.

note

💸 Price per generation is another big difference between the Smol AI and Wasp AI. Because more work is being done by Wasp’s compiler and less by GPT, each app costs about ~$0.10-$0.20 to generate (although Wasp covers the cost and allows you to use it for free), whereas to generate complex full-stack apps with Smol-Developer can cost upwards of ~$10.00!

Plus, there are plenty of YouTubers who’ve created videos about the process of using Smol-Developer and it seems they all come to similar conclusions: you need to create a very detailed and explicit prompt in order to get a working prototype (In fact, in AI Jason’s Smol-AI video above, he mentioned that he got the best results out of the box when prompting Smol-Developer to write everything to one file only — of course this limits you to generating simple apps only that are not so easy to continue from manually).

Thoughts & Further Considerations

At their core, SmolAI and WaspAI function quite similarly, by first prompting the LLM to create a plan for the app’s architecture, and then to execute on that plan, file by file.

But because Smol-Developer aims to be able to generate a wider range of apps, the expectation is on the Developer (or “Prompt Engineer”) to create a highly detailed, explicit prompt, which is more akin to a Product Requirement Doc that a Product Designer would write. This can take a few iterations to get right and pushes Smol-Developer in the direction of “Natural Language Programming” tool.

On the other hand, Wasp’s GPT Web App Generator has a lot of prompting and programming going on behind the scenes, abstracted away from the user and hidden within the Generator’s code and Wasp’s compiler. Wasp comes with a lot of knowledge baked in and already has a good idea of what it wants to build, which means the user has less to think about it. This means that we’re more likely to get a working complex prototype from a short, simple prompt, but we have less flexibility in the kinds of apps we’re able to create — we always get a full-stack web app.

In general, Wasp is like a junior developer specialized in web dev and has a lot of experience with a specific stack, while Smol AI is a junior developer that’s a generalist who is more versatile, but has less specific knowledge and experience with web dev 🙂

Smol AIWasp AI
🧑‍💻 Types of AppsVariedFull-stack Web Apps
🗯 Programming LanguagesAll TypesJavaScript/TypeScript
📈 Complexity of Generated AppSimple to MediumMedium to Complex
💰 Price per Generation — via OpenAI’s API$0.80 to $10.00$0.10 to $0.20 
💳 Payment Methodbring your own API keyfree — paid for by Wasp
🐛 DebuggingYes, if you’re willing to tinkerBuilt-in, but limited
🗣 Type of Prompt NeededComplex and detailed, 1 or more pages (e.g. an entire Product Requirement Doc)Simple, 1-3 paragraphs
😎 Intended UserEngineers, Product Designers wanting to generate a broad range of simple prototypesWeb Devs, Product Designers that want a feature rich full-stack web app prototype

Other big differences lie within:

  1. Error Correction upon Code Creation
    1. Smol AI initially had a debugging script, but this has temporarily deprecated due to the fact that it expects the entire codebase when debugging, and current 32k and 100k token context windows are only available in private beta for GPT4 and Anthropic at the moment.
    2. Wasp AI has some error correction baked into its process, as the structure of a Wasp app is more defined and the range of errors are more predictable.
  2. Price per app generation via OpenAI’s chat completion endpoints
    1. Smol AI can cost anywhere from ~$0.80 to $10.00 depending on the complexity of the app.
    2. Wasp AI costs ~$0.10 to $0.20 per app, when using the default mix of GPT 4 and GPT 3.5 turbo, but Wasp covers the bill here. If you choose to run it just with GPT4, then the cost is 10x at $1.00 to $2.00 per generation and you have to provide your own API key.
  3. User Interface
    1. Smol Developer works through the command line and has minimal logging and process feedback
    2. Wasp AI currently uses a clean web app UI with more logging and feedback, as well as through the command line without a UI (you have to download the experimental Wasp release to do so at this time).

Overall, both solutions produce amazing results, allowing solo developers or teams iterate on ideas and generate prototypes faster than before. But they still have a lot of room for improvement.

For example, what these tools lack the most at the moment is in interactive debugging and incremental generation. It would be great if they could allow the user to generate additional code and fix problems in the codebase on the fly, rather than having to go back, rewrite the prompt, and regenerate an entire new codebase.

I’m not aware of the Smol AI roadmap, but seeing that it’s received a grant from Vercel’s AI accelerator program, I’m sure we will be seeing development on it continue and the tool improve (let me know in the comments if you do have some insight here).

On the other hand, as I’m a member of the Wasp team, I can confidently say that Wasp will soon be adding the initial generation process and interactive debugging into Wasp’s command line interface!

So I definitely think it’s early days and that these tools will continue to progress — and continue to produce more impressive results 🚀

Which Tool Should You Use?

Obviously, there can be no clear winner here as the answer to question of which tool you should use as your next “AI Junior Developer” depends largely on your goals.

Are you looking for a tool that can generate a broad range of simple apps? And are you interested in learning more about building AI-assisted coding tools and natural language programming and don’t mind tweaking and tinkering for a while? Well then, Smol-Developer is what you’re looking for!

Do you want to generate a working full-stack React/Node app prototype with all the bells and whistles as quickly and easily as possible? Head straight for Wasp’s GPT Web App Generator!

Help me help you

🌟 If you haven’t yet, please star us on GitHub, especially if you found this useful! If you do, it helps support us in creating more content like this. And if you don’t… well, we will deal with it, I guess.

https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif

In general, as Jason “AI Jason” Zhou said:

I’m really excited about [AI-assisted coding tools] because if I want to user-test a certain product idea I can ask it to build a prototype very, very quickly, and test with real users”

Jason makes a great point here, that these tools don’t really have the capacity to replace Junior Developers entirely in their current capacity (although they will surely improve in the future), but they do improve the speed and ease with which we can try out novel ideas!

I personally believe that in the near future we will see more domain-specific AI-assisted tools like Wasp’s GPT Web App Generator because of the performance gains they bring to the end user. Code agents that are focused on a niche can produce better results out of the box due to the embedded knowledge. In the future, I think we can expect a lot of agents that are each tailored towards fulfilling a specific task.

But don’t just take my word for it. Go ahead try out Smol-Developer and the GPT Web App Generator for yourself and let me know what you think in the comments!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html b/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html index 2ead8e2f54..78b6ac65c1 100644 --- a/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html +++ b/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html @@ -19,14 +19,14 @@ - - + +
-

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

· 22 min read
Vinny

TL;DR

WebSockets allow your app to have “real time” features, where updates are instant because they’re passed on an open, two-way channel. This is a different from CRUD apps, which usually use HTTP requests that must establish a connection, send a request, receive a response, and then close the connection.

To use WebSockets in your React app, you’ll need a dedicated server, such as an ExpressJS app with NodeJS, in order to maintain a persistent connection.

Unfortunately, serverless solutions (e.g. NextJS, AWS lambda) don’t natively support WebSockets. Bummer. 😞

Why not? Well, serverless services turn on and off depending on if a request is coming in. With WebSockets, we need this “always on” connection that only a dedicated server can provide (although you can pay for third-party services as a workaround).

Luckily, we’re going to talk about two great ways you can implement them:

  1. Advanced: Implementing and configuring it yourself with React, NodeJS, and Socket.IO
  2. Easy: By using Wasp, a full-stack React-NodeJS framework, to configure and integrate Socket.IO into your app for you.

These methods allow you to build fun stuff, like this instantly updating “voting with friends” app we built here:

You can try out the live demo app here.
+

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

· 22 min read
Vinny

TL;DR

WebSockets allow your app to have “real time” features, where updates are instant because they’re passed on an open, two-way channel. This is a different from CRUD apps, which usually use HTTP requests that must establish a connection, send a request, receive a response, and then close the connection.

To use WebSockets in your React app, you’ll need a dedicated server, such as an ExpressJS app with NodeJS, in order to maintain a persistent connection.

Unfortunately, serverless solutions (e.g. NextJS, AWS lambda) don’t natively support WebSockets. Bummer. 😞

Why not? Well, serverless services turn on and off depending on if a request is coming in. With WebSockets, we need this “always on” connection that only a dedicated server can provide (although you can pay for third-party services as a workaround).

Luckily, we’re going to talk about two great ways you can implement them:

  1. Advanced: Implementing and configuring it yourself with React, NodeJS, and Socket.IO
  2. Easy: By using Wasp, a full-stack React-NodeJS framework, to configure and integrate Socket.IO into your app for you.

These methods allow you to build fun stuff, like this instantly updating “voting with friends” app we built here:

You can try out the live demo app here.
And if you just want the app code, it's available here on GitHub.

Why WebSockets?

So, imagine you're at a party sending text messages to a friend to tell them what food to bring.

Now, wouldn’t it be easier if you called your friend on the phone so you could talk constantly, instead of sending sporadic messages? That's pretty much what WebSockets are in the world of web applications.

For example, traditional HTTP requests (e.g. CRUD/RESTful) are like those text messages — your app has to ask the server every time it wants new information, just like you had to send a text message to your friend every time you thought of food for your party.

But with WebSockets, once a connection is established, it remains open for constant, two-way communication, so the server can send new information to your app the instant it becomes available, even if the client didn’t ask for it.

This is perfect for real-time applications like chat apps, game servers, or when you're keeping track of stock prices. For example, apps like Google Docs, Slack, WhatsApp, Uber, Zoom, and Robinhood all use WebSockets to power their real-time communication features.

https://media3.giphy.com/media/26u4hHj87jMePiO3u/giphy.gif?cid=7941fdc6hxgjnub1rcs80udcj652956fwmm4qhxsmk6ldxg7&ep=v1_gifs_search&rid=giphy.gif&ct=g

So remember, when your app and server have a lot to talk about, go for WebSockets and let the conversation flow freely!

How WebSockets Work

If you want real-time capabilities in your app, you don’t always need WebSockets. You can implement similar functionality by using resource-heavy processes, such as:

  1. long-polling, e.g. running setInterval to periodically hit the server and check for updates.
  2. one-way “server-sent events”, e.g. keeping a unidirectional server-to-client connection open to receive new updates from the server only.

1. HTTP handshake, 2. two-way instant communication, 3. close connection

WebSockets, on the other hand, provide a two-way (aka “full-duplex”) communication channel between the client and server.

Once established via an HTTP “handshake”, the server and client can freely exchange information instantly before the connection is finally closed by either side.

Although introducing WebSockets does add complexity due to asynchronous and event-driven components, choosing the right libraries and frameworks can make it easy.

In the sections below, we will show you two ways to implement WebSockets into a React-NodeJS app:

  1. Configuring it yourself alongside your own standalone Node/ExpressJS server
  2. Letting Wasp, a full-stack framework with superpowers, easily configure it for you

Adding WebSockets Support in a React-NodeJS App

What You Shouldn’t Use: Serverless Architecture

But first, here’s a “heads up” for you: despite being a great solution for certain use-cases, serverless solutions are not the right tool for this job.

That means, popular frameworks and infrastructure, like NextJS and AWS Lambda, do not support WebSockets integration out-of-the-box.

Instead of running on a dedicated, traditional server, such solutions utilize serverless functions (also known as lambda functions), which are designed to execute and complete a task as soon as a request comes in. It’s as if they “turn on” when the request comes in, and then “turn off” once it’s completed.

This serverless architecture is not ideal for keeping a WebSocket connection alive because we want a persistent, “always-on” connection.

That’s why you need a “serverful” architecture if you want to build real-time apps. And although there is a workaround to getting WebSockets on a serverless architecture, like using third-party services, this has a number of drawbacks:

  • Cost: these services exist as subscriptions and can get costly as your app scales
  • Limited Customization: you’re using a pre-built solution, so you have less control
  • Debugging: fixing errors gets more difficult, as your app is not running locally

Using ExpressJS with Socket.IO — Complex/Customizable Method

Okay, let's start with the first, more traditional approach: creating a dedicated server for your client to establish a two-way communication channel with.

This method is more advanced and involves a bit more complexity, but allows for more fine-tuned customization. If you're looking for a straightforward, easier way to bring WebSockets to your React/NodeJS app, we'll get to that in the section below

note

👨‍💻 If you want to code along you can follow the instructions below. Alternatively, if you just want to see the finished React-NodeJS full-stack app, check out the github repo here

In this exampple, we’ll be using ExpressJS with the Socket.IO library. Although there are others out there, Socket.IO is a great library that makes working with WebSockets in NodeJS easier.

If you want to code along, first clone the start branch:

git clone --branch start https://github.com/vincanger/websockets-react.git

You’ll notice that inside we have two folders:

  • 📁 ws-client for our React app
  • 📁 ws-server for our ExpressJS/NodeJS server

Let’s cd into the server folder and install the dependencies:

cd ws-server && npm install

We also need to install the types for working with typescript:

npm i --save-dev @types/cors

Now run the server, using the npm start command in your terminal.

You should see listening on *:8000 printed to the console!

At the moment, this is what our index.ts file looks like:

import cors from 'cors';
import express from 'express';

const app = express();
app.use(cors({ origin: '*' }));
const server = require('http').createServer(app);

app.get('/', (req, res) => {
res.send(`<h1>Hello World</h1>`);
});

server.listen(8000, () => {
console.log('listening on *:8000');
});

There’s not much going on here, so let’s install the Socket.IO package and start adding WebSockets to our server!

First, let’s kill the server with ctrl + c and then run:

npm install socket.io

Let’s go ahead and replace the index.ts file with the following code. I know it’s a lot of code, so I’ve left a bunch of comments that explain what’s going on ;):

import cors from 'cors';
import express from 'express';
import { Server, Socket } from 'socket.io';

type PollState = {
question: string;
options: {
id: number;
text: string;
description: string;
votes: string[];
}[];
};
interface ClientToServerEvents {
vote: (optionId: number) => void;
askForStateUpdate: () => void;
}
interface ServerToClientEvents {
updateState: (state: PollState) => void;
}
interface InterServerEvents { }
interface SocketData {
user: string;
}

const app = express();
app.use(cors({ origin: 'http://localhost:5173' })); // this is the default port that Vite runs your React app on
const server = require('http').createServer(app);
// passing these generic type parameters to the `Server` class
// ensures data flowing through the server are correctly typed.
const io = new Server<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>(server, {
cors: {
origin: 'http://localhost:5173',
methods: ['GET', 'POST'],
},
});

// this is middleware that Socket.IO uses on initiliazation to add
// the authenticated user to the socket instance. Note: we are not
// actually adding real auth as this is beyond the scope of the tutorial
io.use(addUserToSocketDataIfAuthenticated);

// the client will pass an auth "token" (in this simple case, just the username)
// to the server on initialize of the Socket.IO client in our React App
async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) => void) {
const user = socket.handshake.auth.token;
if (user) {
try {
socket.data = { ...socket.data, user: user };
} catch (err) {}
}
next();
}

// the server determines the PollState object, i.e. what users will vote on
// this will be sent to the client and displayed on the front-end
const poll: PollState = {
question: "What are eating for lunch ✨ Let's order",
options: [
{
id: 1,
text: 'Party Pizza Place',
description: 'Best pizza in town',
votes: [],
},
{
id: 2,
text: 'Best Burger Joint',
description: 'Best burger in town',
votes: [],
},
{
id: 3,
text: 'Sus Sushi Place',
description: 'Best sushi in town',
votes: [],
},
],
};

io.on('connection', (socket) => {
console.log('a user connected', socket.data.user);

// the client will send an 'askForStateUpdate' request on mount
// to get the initial state of the poll
socket.on('askForStateUpdate', () => {
console.log('client asked For State Update');
socket.emit('updateState', poll);
});

socket.on('vote', (optionId: number) => {
// If user has already voted, remove their vote.
poll.options.forEach((option) => {
option.votes = option.votes.filter((user) => user !== socket.data.user);
});
// And then add their vote to the new option.
const option = poll.options.find((o) => o.id === optionId);
if (!option) {
return;
}
option.votes.push(socket.data.user);
// Send the updated PollState back to all clients
io.emit('updateState', poll);
});

socket.on('disconnect', () => {
console.log('user disconnected');
});
});

server.listen(8000, () => {
console.log('listening on *:8000');
});

Great, start the server again with npm start and let’s add the Socket.IO client to the front-end.

cd into the ws-client directory and run

cd ../ws-client && npm install

Next, start the development server with npm run dev and you should see the hardcoded starter app in your browser:

You may have noticed that poll does not match the PollState from our server. We need to install the Socket.IO client and set it all up in order start our real-time communication and get the correct poll from the server.

Go ahead and kill the development server with ctrl + c and run:

npm install socket.io-client

Now let’s create a hook that initializes and returns our WebSocket client after it establishes a connection. To do that, create a new file in ./ws-client/src called useSocket.ts:

import { useState, useEffect } from 'react';
import socketIOClient, { Socket } from 'socket.io-client';

export type PollState = {
question: string;
options: {
id: number;
text: string;
description: string;
votes: string[];
}[];
};
interface ServerToClientEvents {
updateState: (state: PollState) => void;
}
interface ClientToServerEvents {
vote: (optionId: number) => void;
askForStateUpdate: () => void;
}

export function useSocket({endpoint, token } : { endpoint: string, token: string }) {
// initialize the client using the server endpoint, e.g. localhost:8000
// and set the auth "token" (in our case we're simply passing the username
// for simplicity -- you would not do this in production!)
// also make sure to use the Socket generic types in the reverse order of the server!
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = socketIOClient(endpoint, {
auth: {
token: token
}
})
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
console.log('useSocket useEffect', endpoint, socket)

function onConnect() {
setIsConnected(true)
}

function onDisconnect() {
setIsConnected(false)
}

socket.on('connect', onConnect)
socket.on('disconnect', onDisconnect)

return () => {
socket.off('connect', onConnect)
socket.off('disconnect', onDisconnect)
}
}, [token]);

// we return the socket client instance and the connection state
return {
isConnected,
socket,
};
}

Now let’s go back to our main App.tsx page and replace it with the following code (again I’ve left comments to explain):

import { useState, useMemo, useEffect } from 'react';
import { Layout } from './Layout';
import { Button, Card } from 'flowbite-react';
import { useSocket } from './useSocket';
import type { PollState } from './useSocket';

const App = () => {
// set the PollState after receiving it from the server
const [poll, setPoll] = useState<PollState | null>(null);

// since we're not implementing Auth, let's fake it by
// creating some random user names when the App mounts
const randomUser = useMemo(() => {
const randomName = Math.random().toString(36).substring(7);
return `User-${randomName}`;
}, []);

// 🔌⚡️ get the connected socket client from our useSocket hook!
const { socket, isConnected } = useSocket({ endpoint: `http://localhost:8000`, token: randomUser });

const totalVotes = useMemo(() => {
return poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0;
}, [poll]);

// every time we receive an 'updateState' event from the server
// e.g. when a user makes a new vote, we set the React's state
// with the results of the new PollState
socket.on('updateState', (newState: PollState) => {
setPoll(newState);
});

useEffect(() => {
socket.emit('askForStateUpdate');
}, []);

function handleVote(optionId: number) {
socket.emit('vote', optionId);
}

return (
<Layout user={randomUser}>
<div className='w-full max-w-2xl mx-auto p-8'>
<h1 className='text-2xl font-bold'>{poll?.question ?? 'Loading...'}</h1>
<h2 className='text-lg italic'>{isConnected ? 'Connected ✅' : 'Disconnected 🛑'}</h2>
{poll && <p className='leading-relaxed text-gray-500'>Cast your vote for one of the options.</p>}
{poll && (
<div className='mt-4 flex flex-col gap-4'>
{poll.options.map((option) => (
<Card key={option.id} className='relative transition-all duration-300 min-h-[130px]'>
<div className='z-10'>
<div className='mb-2'>
<h2 className='text-xl font-semibold'>{option.text}</h2>
<p className='text-gray-700'>{option.description}</p>
</div>
<div className='absolute bottom-5 right-5'>
{randomUser && !option.votes.includes(randomUser) ? (
<Button onClick={() => handleVote(option.id)}>Vote</Button>
) : (
<Button disabled>Voted</Button>
)}
</div>
{option.votes.length > 0 && (
<div className='mt-2 flex gap-2 flex-wrap max-w-[75%]'>
{option.votes.map((vote) => (
<div
key={vote}
className='py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm'
>
<div className='w-2 h-2 bg-green-500 rounded-full mr-2'></div>
<div className='text-gray-700'>{vote}</div>
</div>
))}
</div>
)}
</div>
<div className='absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10'>
{option.votes.length} / {totalVotes}
</div>
<div
className='absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300'
style={{
width: `${totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0}%`,
}}
></div>
</Card>
))}
</div>
)}
</div>
</Layout>
);
};
export default App;

Go ahead now and start the client with npm run dev. Open another terminal window/tab, cd into the ws-server directory and run npm start.

If we did that correctly, we should be seeing our finished, working, REAL TIME app! 🙂

It looks and works great if you open it up in two or three browser tabs. Check it out:

Nice!

So we’ve got the core functionality here, but as this is just a demo, there are a couple very important pieces missing that make this app unusable in production.

Mainly, we’re creating a random fake user each time the app mounts. You can check this by refreshing the page and voting again. You’ll see the votes just add up, as we’re creating a new random user each time. We don’t want that!

We should instead be authenticating and persisting a session for a user that’s registered in our database. But another problem: we don’t even have a database at all in this app!

You can start to see the how the complexity add ups for even just a simple voting feature

Luckily, our next solution, Wasp, has integrated Authentication and Database Management. Not to mention, it also takes care of a lot of the WebSockets configuration for us.

So let’s go ahead and give that a go!

Implementing WebSockets with Wasp — Easier/Less Config Method

Because Wasp is an innovative full-stack framework, it makes building React-NodeJS apps quick and developer-friendly.

Wasp has lots of time-saving features, including WebSocket support via Socket.IO, Authentication, Database Management, and Full-stack type-safety out-of-the box.

Wasp can take care of all this heavy lifting for you because of its use of a config file, which you can think of like a set of instructions that the Wasp compiler uses to help glue your app together.

To see it in action, let's implement WebSocket communication using Wasp by following these steps

tip

If you just want to see finished app’s code, you can check out the GitHub repo here

  1. Install Wasp globally by running the following command in your terminal:
curl -sSL https://get.wasp-lang.dev/installer.sh | sh 

If you want to code along, first clone the start branch of the example app:

git clone --branch start https://github.com/vincanger/websockets-wasp.git

You’ll notice that the structure of the Wasp app is split:

  • 🐝 a main.wasp config file exists at the root
  • 📁 src/client is our directory for our React files
  • 📁 src/server is our directory for our ExpressJS/NodeJS functions

Let’s start out by taking a quick look at our main.wasp file.

app whereDoWeEat {
wasp: {
version: "^0.13.2"
},
title: "where-do-we-eat",
client: {
rootComponent: import { Layout } from "@src/client/Layout",
},
// 🔐 This is how we get Auth in our app. Easy!
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {}
}
},
}

// 👱 this is the data model for our registered users in our database
entity User {=psl
id Int @id @default(autoincrement())
psl=}

// ...

With this, the Wasp compiler will know what to do and will configure these features for us.

Let’s tell it we want WebSockets, as well. Add the webSocket definition to the main.wasp file, just between auth and dependencies:

app whereDoWeEat {
// ...
webSocket: {
fn: import { webSocketFn } from "@src/server/ws-server",
},
// ...
}

Now we have to define the webSocketFn. In the ./src/server directory create a new file, ws-server.ts and copy the following code:

import { getUsername } from 'wasp/auth';
import { type WebSocketDefinition } from 'wasp/server/webSocket';

type PollState = {
question: string;
options: {
id: number;
text: string;
description: string;
votes: string[];
}[];
};

interface ServerToClientEvents {
updateState: (state: PollState) => void;
}
interface ClientToServerEvents {
vote: (optionId: number) => void;
askForStateUpdate: () => void;
}
interface InterServerEvents {}

export const webSocketFn: WebSocketDefinition<ClientToServerEvents, ServerToClientEvents, InterServerEvents> = (
io,
_context
) => {
const poll: PollState = {
question: "What are eating for lunch ✨ Let's order",
options: [
{
id: 1,
text: 'Party Pizza Place',
description: 'Best pizza in town',
votes: [],
},
{
id: 2,
text: 'Best Burger Joint',
description: 'Best burger in town',
votes: [],
},
{
id: 3,
text: 'Sus Sushi Place',
description: 'Best sushi in town',
votes: [],
},
],
};
io.on('connection', (socket) => {
if (!socket.data.user) {
console.log('Socket connected without user');
return;
}

const connectionUsername = getUsername(socket.data.user);

console.log('Socket connected: ', connectionUsername);
socket.on('askForStateUpdate', () => {
socket.emit('updateState', poll);
});

socket.on('vote', (optionId) => {
if (!connectionUsername) {
return;
}
// If user has already voted, remove their vote.
poll.options.forEach((option) => {
option.votes = option.votes.filter((username) => username !== connectionUsername);
});
// And then add their vote to the new option.
const option = poll.options.find((o) => o.id === optionId);
if (!option) {
return;
}
option.votes.push(connectionUsername);
io.emit('updateState', poll);
});

socket.on('disconnect', () => {
console.log('Socket disconnected: ', connectionUsername);
});
});
};

You may have noticed that there’s a lot less configuration and boilerplate needed here in the Wasp implementation. That’s because the:

  • endpoints,
  • authentication,
  • and Express and Socket.IO middleware

are all being handled for you by Wasp. Noice!

Let’s go ahead now and run the app to see what we have at this point.

First, we need to initialize the database so that our Auth works correctly. This is something we didn’t do in the previous example due to high complexity, but is easy to do with Wasp:

wasp db migrate-dev

Once that’s finished, run the app (it my take a while on first run to install all depenedencies):

wasp start

You should see a login screen this time. Go ahead and first register a user, then login:

Once logged in, you’ll see the same hardcoded poll data as in the previous example, because, again, we haven’t set up the Socket.IO client on the frontend. But this time it should be much easier.

Why? Well, besides less configuration, another nice benefit of working with TypeScript with Wasp, is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client!

Let’s take a look at how that works now.

In .src/client/MainPage.tsx, replace the contents with the following code:

// Wasp provides us with pre-configured hooks and types based on
// our server code. No need to set it up ourselves!
import { type ServerToClientPayload, useSocket, useSocketListener } from 'wasp/client/webSocket';
import { useAuth } from 'wasp/client/auth';
import { useState, useMemo, useEffect } from 'react';
import { Button, Card } from 'flowbite-react';
import { getUsername } from 'wasp/auth';

const MainPage = () => {
// Wasp provides a bunch of pre-built hooks for us :)
const { data: user } = useAuth();
const [poll, setPoll] = useState<ServerToClientPayload<'updateState'> | null>(null);
const totalVotes = useMemo(() => {
return poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0;
}, [poll]);

const { socket } = useSocket();

const username = user ? getUsername(user) : null;

useSocketListener('updateState', (newState) => {
setPoll(newState);
});

useEffect(() => {
socket.emit('askForStateUpdate');
}, []);

function handleVote(optionId: number) {
socket.emit('vote', optionId);
}

return (
<div className='w-full max-w-2xl mx-auto p-8'>
<h1 className='text-2xl font-bold'>{poll?.question ?? 'Loading...'}</h1>
{poll && <p className='leading-relaxed text-gray-500'>Cast your vote for one of the options.</p>}
{poll && (
<div className='mt-4 flex flex-col gap-4'>
{poll.options.map((option) => (
<Card key={option.id} className='relative transition-all duration-300 min-h-[130px]'>
<div className='z-10'>
<div className='mb-2'>
<h2 className='text-xl font-semibold'>{option.text}</h2>
<p className='text-gray-700'>{option.description}</p>
</div>
<div className='absolute bottom-5 right-5'>
{username && !option.votes.includes(username) ? (
<Button onClick={() => handleVote(option.id)}>Vote</Button>
) : (
<Button disabled>Voted</Button>
)}
{!user}
</div>
{option.votes.length > 0 && (
<div className='mt-2 flex gap-2 flex-wrap max-w-[75%]'>
{option.votes.map((username, idx) => {
return (
<div
key={username}
className='py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm'
>
<div className='w-2 h-2 bg-green-500 rounded-full mr-2'></div>
<div className='text-gray-700'>{username}</div>
</div>
);
})}
</div>
)}
</div>
<div className='absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10'>
{option.votes.length} / {totalVotes}
</div>
<div
className='absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300'
style={{
width: `${totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0}%`,
}}
></div>
</Card>
))}
</div>
)}
</div>
);
};
export default MainPage;

In comparison to the previous implementation, Wasp saved us from having to configure the Socket.IO client, as well as building our own hooks.

Also, hover over the variables in your client-side code, and you’ll see that the types are being automatically inferred for you!

Here’s just one example, but it should work for them all:

Now if you open up a new private/incognito tab, register a new user, and login, you’ll see a fully working, real-time voting app. The best part is, in comparison to the previous approach, we can log out and back in, and our voting data persists, which is exactly what we’d expect from a production grade app. 🎩

Awesome… 😏

Comparing the Two Approaches

Now, just because one approach seems easier, doesn’t always mean it’s always better. Let’s give a quick run-down of the advantages and disadvantages of both the implementations above.

Without WaspWith Wasp
😎 Intended UserSenior Developers, web development teamsFull-stack developers, “Indiehackers”, junior devs
📈 Complexity of CodeMedium-to-HighLow
🚤 SpeedSlower, more methodicalFaster, more integrated
🧑‍💻 LibrariesAnySocket.IO
⛑ Type safetyImplement on both server and clientImplement once on server, inferred by Wasp on client
🎮 Amount of controlHigh, as you determine the implementationOpinionated, as Wasp decides the basic implementation
🐛 Learning CurveComplex: full knowledge of front and backend technologies, including WebSocketsIntermediate: Knowledge of full-stack fundamentals necessary.

Implementing WebSockets Using React, Express.js (Without Wasp)

Advantages:

  1. Control & Flexibility: You can approach the implementation of WebSockets in the way that best suits your project's needs, as well as your choice between a number of different WebSocket libraries, not just Socket.IO.

Disadvantages:

  1. More Code & Complexity: Without the abstractions provided by a framework like Wasp, you might need to write more code and create your own abstractions to handle common tasks. Not to mention the proper configuration of a NodeJS/ExpressJS server (the one provided in the example is very basic)
  2. Manual Type Safety: If you’re working with TypeScript, you have to be more careful typing your event handlers and payload types coming into and going out from the server, or implement a more type-safe approach yourself.

Implementing WebSockets with Wasp (uses React, ExpressJS, and Socket.IO under the hood)

Advantages:

  1. Fully-Integrated/Less code: Wasp provides useful abstractions such as useSocket and useSocketListener hooks for use in React components (on top of other features like Auth, Async Jobs, Email-sending, DB management, and Deployment), simplifying the client-side code, and allowing for full integration with less configuration.
  2. Type Safety: Wasp facilitates full-stack type safety for WebSocket events and payloads. This reduces the likelihood of runtime errors due to mismatched data types and saves you from writing even more boilerplate.

Disadvantages:

  1. Learning curve: Developers unfamiliar with Wasp will need to learn the framework to effectively use it.
  2. Less control: While Wasp provides a lot of conveniences, it abstracts away some of the details, giving developers slightly less control over certain aspects of socket management.

Conclusion

In general, how you add WebSockets to your React app depends on the specifics of your project, your comfort level with the available tools, and the trade-offs you're willing to make between ease of use, control, and complexity.

Don’t forget, if you want to check out the full finished code from our “Lunch Voting” example full-stack app, go here: https://github.com/vincanger/websockets-wasp

And if you know of a better, cooler, sleeker way of implementing WebSockets into your apps, let us know in the comments below

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html b/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html index 7346da6717..375ed5dc9c 100644 --- a/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html +++ b/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html @@ -19,17 +19,17 @@ - - + +
-

Using Product Requirement Documents to Generate Better Web Apps with AI

· 9 min read
Vinny

I’m an indiehacker that likes creating lots of fun side-projects, like my SaaS app CoverLetterGPT with ~4,000 users. That’s why I've been on the lookout for AI-assisted coding tools to help me kickstart new full-stack web apps as quickly as possible.

I tried out a bunch, but found that most of them produced codebases that were too simple to work with, or getting a good result was just about as time consuming as coding it myself.

But through the process of trying out different tools and methods, I stumbled across a hack that helped me create comprehensive, functional codebases for full-stack apps with Auth, API routes, Tailwind CSS, DB management, and other more complex features.

The trick? Ask ChatGPT to write you a detailed Product Requirement Doc for the app you’d like to create, and then pass this to Wasp’s GPT Web App Generator.

Image description

The results are really surprising and give you a far better starter codebase than the other tools I’ve tried (mainly due to the specificity of the generator itself).

And best of all, its free to use! 🤑

Intro

I’m a self-taught, full-stack web developer and I have a lot of fun building side projects.

For example, the side project I’m most proud of is an open-source cover letter generator SaaS App, CoverLetterGPT, which has close to 4,000 users!

I also have a lot of ridiculous side-project ideas, like this app that can turn your favorite tech influencer’s YouTube videos into a drinking game. 🤣

That’s why I’ve been trying out lots of AI-assisted coding tools to generate fully-functional, full-stack web apps as quickly as possible.

There are the obvious tools at the moment, like using ChatGPT and Copilot within your IDE, but new ones are popping up all the time, especially those that act as AI assistants or “agents”.

I’ve gotten a chance to try out some of them, and I even wrote a long-form comparison piece where I put two such tools to the test, so check that out if you’re interested.

But there’s a major problem with these tools: even though they’re able to generate some good boilerplate code, they often include a lot of errors and don’t make the developer's job that much easier in the end.

Where the problem lies

On paper, AI-assisted coding tools generally save devs time and effort, especially when it comes to isolated code snippets.

On one hand, we have tools like ChatGPT and Copilot, which aid you with refactoring, fixing errors, or generating a snippet of code. It's much like assembling a jigsaw puzzle, where the tools serve you the next piece that fits the immediate gap.

But coding isn't just about filling the next available space; it’s about envisioning the entire picture, understanding the broader system and how different pieces interrelate.

https://media3.giphy.com/media/SrnCKS6s02XT2tw6kz/giphy.gif?cid=7941fdc6b01lfcj3taubztyp823itz03hhy9qx8p0mslbtij&ep=v1_gifs_search&rid=giphy.gif&ct=g

AI-assisted coding tools that behave more like agents have the potential to understand this broader context needed to generate larger codebases, but it’s easier said than done. Currently, most of the tools out there end up generating code that comes full of errors.

Worst of all, some of the code they output can be so messy it actually means more work for you.

How to fix it

AI assistants, much like novice apprentices, need a comprehensive understanding of what they should work towards. To achieve this, you need to craft a detailed outline along with a comprehensive set of instructions to give the AI as much context as possible.

You essentially want to be taking on the role of a Product Manager/Designer and be giving the AI a Product Requirement Document (PRD), i.e. an authoritative document that clearly outlines the

- - + + \ No newline at end of file diff --git a/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html b/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html index 37fc3e1cf4..b4b5b74a75 100644 --- a/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html +++ b/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html @@ -19,15 +19,15 @@ - - + +
-

Build your own AI Meme Generator & learn how to use OpenAI's function calls

· 31 min read
Vinny

Table of Contents

# TL;DR

In this two-part tutorial, we’re going to build a full-stack instant Meme Generator app using:

You check out a deployed version of the app we’re going to build here: The Memerator

If you just want to see the code for the finished app, check out the Memerator’s GitHub Repo

# Intro

Call Me, Maybe

With OpenAI’s chat completions API, developers are now able to do some really cool stuff. It basically enables ChatGPT functionality, but in the form of a callable API you can integrate into any app.

But when working with the API, a lot of devs wanted GPT to give back data in a format, like JSON, that they could use in their app’s functions.

Unfortunately, if you asked ChatGPT to return the data in a certain format, it wouldn’t always get it right. Which is why OpenAI released function calling.

As they describe it, function calling allows devs to “… describe functions to GPT, and have the model intelligently choose to output a JSON object containing arguments to call those functions.”

This is a great way to turn natural language into an API call.

So what better way to learn how to use GPT’s function calling feature than to use it to call Imgflip.com’s meme creator API!?

Image description

## Let’s Build

In this two-part tutorial, we’re going to build a full-stack React/NodeJS app with:

  • Authentication
  • Meme generation via OpenAI’s function calling and ImgFlip.com’s API
  • Daily cron job to fetch new meme templates
  • Meme editing and deleting
  • and more!

Image description

I already deployed a working version of this app that you can try out here: https://damemerator.netlify.app — so give it a go and let’s get… going.

In Part 1 of this tutorial, we will get the app set up and generating and displaying memes.

In Part 2, we will add more functionality, like recurring cron jobs to fetch more meme templates, and the ability to edit and delete memes.

BTW, two quick tips:

  1. if you need to reference the app’s finished code at any time to help you with this tutorial, you can check out the app’s GitHub Repo here.
  2. if you have any questions, feel free to hop into the Wasp Discord Server and ask us!

Part 1

Configuration

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

Set up your Wasp project

First, install Wasp by running this in your terminal:

curl -sSL <https://get.wasp-lang.dev/installer.sh> | sh

Next, let’s clone the start branch of the Memerator app that I’ve prepared for you:

git clone -b start https://github.com/vincanger/memerator.git

Then navigate into the Memerator directory and open up the project in VS Code:

cd Memerator && code .

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s check out the main.wasp file first. You can think of it as the “skeleton”, or instructions, of your app. This file configures most of your full-stack app for you 🤯:

app Memerator {
wasp: {
version: "^0.11.3"
},
title: "Memerator",
client: {
rootComponent: import { Layout } from "@client/Layout",
},
db: {
system: PostgreSQL,
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
auth: {
userEntity: User,
methods: {
usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
dependencies: [
("openai", "4.2.0"),
("axios", "^1.4.0"),
("react-icons", "4.10.1"),
]
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
memes Meme[]
isAdmin Boolean @default(false)
credits Int @default(2)
psl=}

entity Meme {=psl
id String @id @default(uuid())
url String
text0 String
text1 String
topics String
audience String
template Template @relation(fields: [templateId], references: [id])
templateId String
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
psl=}

entity Template {=psl
id String @id @unique
name String
url String
width Int
height Int
boxCount Int
memes Meme[]
psl=}

route HomePageRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/Home",
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup"
}

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)
  • client-side pages & routes

You might have also noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

Also, make sure you install the Wasp VS code extension so that you get nice syntax highlighting and the best overall dev experience.

Setting up the Database

We still need to get a Postgres database setup.

Usually this can be pretty annoying, but with Wasp it’s really easy.

  1. just have Docker Deskop installed and running,
  2. open up a separate terminal tab/window,
  3. cd into the Memerator directory, and then run
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 

Just leave this terminal tab, along with docker desktop, open and running in the background.

Now, in a different terminal tab, run

wasp db migrate-dev

and make sure to give your database migration a name, like init.

Environment Variables

In the root of your project, you’ll find a .env.server.example file that looks like this:

# set up your own credentials on https://imgflip.com/signup and rename this file to .env.server
# NOTE: make sure you register with Username and Password (not google)
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=

# get your api key from https://platform.openai.com/
OPENAI_API_KEY=

JWT_SECRET=asecretphraseatleastthirtytwocharacterslong

Rename this file to .env.server and follow the instructions in it to get your:

as we will need them to generate our memes 🤡

Start your App

With everything setup correctly, you should now be able to run

wasp start

When running wasp start, Wasp will install all the necessary npm packages, start our NodeJS server on port 3001, and our React client on port 3000.

Head to localhost:3000 in your browser to check it out. We should have the basis for our app that looks like this:

Image description

Generating a Meme

The boilerplate code already has the client-side form set up for generating memes based on:

  • topics
  • intended audience

This is the info we will send to the backend to call the OpenAI API using function calls. We then send this info to the imglfip.com API to generate the meme.

But the /caption_image endpoint of the imgflip API needs the meme template id. And to get that ID we first need to fetch the available meme templates from imgflip’s /get_memes endpoint

So let’s set that up now.

Server-Side Code

Create a new file in src/server/ called utils.ts:

import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';

type GenerateMemeArgs = {
text0: string;
text1: string;
templateId: string;
};

export const fetchMemeTemplates = async () => {
try {
const response = await axios.get('https://api.imgflip.com/get_memes');
return response.data.data.memes;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error fetching meme templates');
}
};

export const generateMemeImage = async (args: GenerateMemeArgs) => {
console.log('args: ', args);

try {
const data = stringify({
template_id: args.templateId,
username: process.env.IMGFLIP_USERNAME,
password: process.env.IMGFLIP_PASSWORD,
text0: args.text0,
text1: args.text1,
});

// Implement the generation of meme using the Imgflip API
const res = await axios.post('https://api.imgflip.com/caption_image', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});

const url = res.data.data.url;

console.log('generated meme url: ', url);

return url as string;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error generating meme image');
}
};

This gives us some utility functions to help us fetch all the meme templates that we can possibly generate meme images with.

Notice that the POST request to the /caption_image endpoint takes the following data:

  • our imgflip username and password
  • ID of the meme template we will use
  • the text for top of the meme, i.e. text0
  • the text for the bottom of the meme, i.e. text1

Image description

The text0 and text1 arguments will generated for us by our lovely friend, ChatGPT. But in order for GPT to do that, we have to set up its API call, too.

To do that, create a new file in src/server/ called actions.ts.

Then go back to your main.wasp config file and add the following Wasp Action at the bottom of the file:

//...

action createMeme {
fn: import { createMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

An Action is a type of Wasp Operation that changes some state on the backend. It’s essentially a NodeJS function that gets called on the server, but Wasp takes care of setting it all up for you.

This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, you just write the business logic!

Image description

If you’ve got the Wasp VS Code extension installed, you’ll see an error (above). Hover over it and click Quick Fix > create function createMeme.

This will scaffold a createMeme function (below) for you in your actions.ts file if the file exists. Pretty Cool!

import { CreateMeme } from '@wasp/actions/types'

type CreateMemeInput = void
type CreateMemeOutput = void

export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
// Implementation goes here
}

You can see that it imports the Action types for you as well.

Because we will be sending the topics array and the intended audience string for the meme from our front-end form, and in the end we will return the newly created Meme entity, that’s what we should define our types as.

Remember, the Meme entity is the database model we defined in our main.wasp config file.

Knowing that, we can change the content of actions.ts to this:

import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
// Implementation goes here
}

Before we implement the rest of the logic, let’s run through how our createMeme function should work and how our Meme will get generated:

  1. fetch the imgflip meme template we want to use
  2. send its name, the topics, and intended audience to OpenAI’s chat completions API
  3. tell OpenAI we want the result back as arguments we can pass to our next function in JSON format, i.e. OpenAI’s function calling
  4. pass those arguments to the imgflip /caption-image endpoint and get our created meme’s url
  5. save the meme url and other info into our DB as a Meme entity

With all that in mind, go ahead and entirely replace the content in our actions.ts with the completed createMeme action:

import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } from './utils.js';

import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

if (context.user.credits === 0 && !context.user.isAdmin) {
throw new HttpError(403, 'You have no credits left');
}

const topicsStr = topics.join(', ');

let templates: Template[] = await context.entities.Template.findMany({});

if (templates.length === 0) {
const memeTemplates = await fetchMemeTemplates();
templates = await Promise.all(
memeTemplates.map(async (template: any) => {
const addedTemplate = await context.entities.Template.upsert({
where: { id: template.id },
create: {
id: template.id,
name: template.name,
url: template.url,
width: template.width,
height: template.height,
boxCount: template.box_count
},
update: {}
});

return addedTemplate;
})
);
}

// filter out templates with box_count > 2
templates = templates.filter((template) => template.boxCount <= 2);
const randomTemplate = templates[Math.floor(Math.random() * templates.length)];

console.log('random template: ', randomTemplate);

const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided`;
const userPrompt = `Topics: ${topicsStr} \n Intended Audience: ${audience} \n Template: ${randomTemplate.name} \n`;

let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
try {
openAIResponse = await openai.chat.completions.create({
messages: [
{ role: 'system', content: sysPrompt },
{ role: 'user', content: userPrompt },
],
functions: [
{
name: 'generateMemeImage',
description: 'Generate meme via the imgflip API based on the given idea',
parameters: {
type: 'object',
properties: {
text0: { type: 'string', description: 'The text for the top caption of the meme' },
text1: { type: 'string', description: 'The text for the bottom caption of the meme' },
},
required: ['templateName', 'text0', 'text1'],
},
},
],
function_call: {
name: 'generateMemeImage',
},
model: 'gpt-4-0613',
});
} catch (error: any) {
console.error('Error calling openAI: ', error);
throw new HttpError(500, 'Error calling openAI');
}

console.log(openAIResponse.choices[0]);

/**
* the Function call returned by openAI looks like this:
*/
// {
// index: 0,
// message: {
// role: 'assistant',
// content: null,
// function_call: {
// name: 'generateMeme',
// arguments: '{\n' +
// ` "text0": "CSS you've been writing all day",\n` +
// ' "text1": "This looks horrible"\n' +
// '}'
// }
// },
// finish_reason: 'stop'
// }
if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');

const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
console.log('gptArgs: ', gptArgs);

const memeIdeaText0 = gptArgs.text0;
const memeIdeaText1 = gptArgs.text1;

console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);

const memeUrl = await generateMemeImage({
templateId: randomTemplate.id,
text0: memeIdeaText0,
text1: memeIdeaText1,
});

const newMeme = await context.entities.Meme.create({
data: {
text0: memeIdeaText0,
text1: memeIdeaText1,
topics: topicsStr,
audience: audience,
url: memeUrl,
template: { connect: { id: randomTemplate.id } },
user: { connect: { id: context.user.id } },
},
});

return newMeme;
};

At this point, the code above should be pretty self-explanatory, but I want to highlight a couple points:

  1. the context object is passed through to all Actions and Queries by Wasp. It contains the Prisma client with access to the DB entities you defined in your main.wasp config file.
  2. We first look for the imgflip meme templates in our DB. If none are found, we fetch them using our fetchTemplates utility function we defined earlier. Then we upsert them into our DB.
  3. There are some meme templates that take more than 2 text boxes, but for this tutorial we’re only using meme templates with 2 text inputs to make it easier.
  4. We choose a random template from this list to use as a basis for our meme (it’s actually a great way to serendipitously generate some interesting meme content).
  5. We give the OpenAI API info about the functions it can create arguments for via the functions and function_call properties, which tell it to always return JSON arguments for our function, generateMemeImage

Great! But once we start generating memes, we will need a way to display them on our front end.

So let’s now create a Wasp Query. A Query works just like an Action, except it’s only for reading data.

Go to src/server and create a new file called queries.ts.

Next, in your main.wasp file add the following code:

//...

query getAllMemes {
fn: import { getAllMemes } from "@server/queries.js",
entities: [Meme]
}

Then in your queries.ts file, add the getAllMemes function:

import HttpError from '@wasp/core/HttpError.js';

import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';

export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
const memeIdeas = await context.entities.Meme.findMany({
orderBy: { createdAt: 'desc' },
include: { template: true },
});

return memeIdeas;
};

Client-Side Code

Now that we’ve got the createMeme and getAllMemes code implemented server-side, let’s hook it up to our client.

Wasp makes it really easy to import the Operations we just created and call them on our front end.

You can do so by going to src/client/pages/Home.tsx and adding the following code to the top of the file:

//...other imports...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

// 😎 😎 😎
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience }); // <--- 😎 😎 😎
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

//...

As you can see, we’ve imported createMeme and getAllMemes (😎).

For getAllMemes, we wrap it in the useQuery hook so that we can fetch and cache the data. On the other hand, our createMeme Action gets called in handleGenerateMeme which we will call when submit our form.

Rather than adding code to the Home.tsx file piece-by-piece, here is the file with all the code to generate and display the memes. Go ahead and replace all of Home.tsx with this code and I’ll explain it in more detail below:

import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
import {
AiOutlinePlusCircle,
AiOutlineMinusCircle,
AiOutlineRobot,
} from 'react-icons/ai';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

const history = useHistory();
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

const handleDeleteMeme = async (id: string) => {
//...
};

if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Welcome to Memerator!</h1>
<p className='mb-4'>Start generating meme ideas by providing topics and intended audience.</p>
<form onSubmit={handleGenerateMeme}>
<div className='mb-4 max-w-[500px]'>
<label htmlFor='topics' className='block font-bold mb-2'>
Topics:
</label>
{topics.map((topic, index) => (
<input
key={index}
type='text'
id='topics'
value={topic}
onChange={(e) => {
const updatedTopics = [...topics];
updatedTopics[index] = e.target.value;
setTopics(updatedTopics);
}}
className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
))}
<div className='flex items-center my-2 gap-1'>
<button
type='button'
onClick={() => topics.length < 3 && setTopics([...topics, ''])}
className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
>
<AiOutlinePlusCircle /> Add Topic
</button>
{topics.length > 1 && (
<button
onClick={() => setTopics(topics.slice(0, -1))}
className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
>
<AiOutlineMinusCircle /> Remove Topic
</button>
)}
</div>
</div>
<div className='mb-4'>
<label htmlFor='audience' className='block font-bold mb-2'>
Intended Audience:
</label>
<input
type='text'
id='audience'
value={audience}
onChange={(e) => setAudience(e.target.value)}
className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
</div>
<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${
isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineRobot />
{!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
</button>
</form>

{!!memes && memes.length > 0 ? (
memes.map((memeIdea) => (
<div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
<img src={memeIdea.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>
</div>
</div>
{/* TODO: implement edit and delete meme features */}
</div>
))
) : (
<div className='flex justify-center mt-5'> :( no memes found</div>
)}
</div>
);
}

There are two things I want to point out about this code:

  1. The useQuery hook calls our getAllMemes Query when the component mounts. It also caches the result for us, as well as automatically re-fetching whenever we add a new Meme to our DB via createMeme. This means our page will reload automatically whenever a new meme is generated.
  2. The useAuth hook allows us to fetch info about our logged in user. If the user isn’t logged in, we force them to do so before they can generate a meme.

These are really cool Wasp features that make your life as a developer a lot easier 🙂

So go ahead now and try and generate a meme. Here’s the one I just generated:

Image description

Haha. Pretty good!

Now wouldn’t it be cool though if we could edit and delete our memes? And what if we could expand the set of meme templates for our generator to use? Wouldn’t that be cool, too?

Yes, it would be. So let’s do that.

Part 2.

So we’ve got ourselves a really good basis for an app at this point.

We’re using OpenAI’s function calling feature to explain a function to GPT, and get it to return results for us in a format we can use to call that function.

This allows us to be certain GPT’s result will be usable in further parts of our application and opens up the door to creating AI agents.

If you think about it, we’ve basically got ourselves a really simple Meme generating “agent”. How cool is that?!

Fetching & Updating Templates with Cron Jobs

To be able to generate our meme images via ImgFlip’s API, we have to choose and send a meme template id to the API, along with the text arguments we want to fill it in with.

For example, the Grandma Finds Internet meme template has the following id:

Image description

But the only way for us to get available meme templates from ImgFlip is to send a GET request to +

Build your own AI Meme Generator & learn how to use OpenAI's function calls

· 31 min read
Vinny

Table of Contents

# TL;DR

In this two-part tutorial, we’re going to build a full-stack instant Meme Generator app using:

You check out a deployed version of the app we’re going to build here: The Memerator

If you just want to see the code for the finished app, check out the Memerator’s GitHub Repo

# Intro

Call Me, Maybe

With OpenAI’s chat completions API, developers are now able to do some really cool stuff. It basically enables ChatGPT functionality, but in the form of a callable API you can integrate into any app.

But when working with the API, a lot of devs wanted GPT to give back data in a format, like JSON, that they could use in their app’s functions.

Unfortunately, if you asked ChatGPT to return the data in a certain format, it wouldn’t always get it right. Which is why OpenAI released function calling.

As they describe it, function calling allows devs to “… describe functions to GPT, and have the model intelligently choose to output a JSON object containing arguments to call those functions.”

This is a great way to turn natural language into an API call.

So what better way to learn how to use GPT’s function calling feature than to use it to call Imgflip.com’s meme creator API!?

Image description

## Let’s Build

In this two-part tutorial, we’re going to build a full-stack React/NodeJS app with:

  • Authentication
  • Meme generation via OpenAI’s function calling and ImgFlip.com’s API
  • Daily cron job to fetch new meme templates
  • Meme editing and deleting
  • and more!

Image description

I already deployed a working version of this app that you can try out here: https://damemerator.netlify.app — so give it a go and let’s get… going.

In Part 1 of this tutorial, we will get the app set up and generating and displaying memes.

In Part 2, we will add more functionality, like recurring cron jobs to fetch more meme templates, and the ability to edit and delete memes.

BTW, two quick tips:

  1. if you need to reference the app’s finished code at any time to help you with this tutorial, you can check out the app’s GitHub Repo here.
  2. if you have any questions, feel free to hop into the Wasp Discord Server and ask us!

Part 1

Configuration

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

Set up your Wasp project

First, install Wasp by running this in your terminal:

curl -sSL <https://get.wasp-lang.dev/installer.sh> | sh

Next, let’s clone the start branch of the Memerator app that I’ve prepared for you:

git clone -b start https://github.com/vincanger/memerator.git

Then navigate into the Memerator directory and open up the project in VS Code:

cd Memerator && code .

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s check out the main.wasp file first. You can think of it as the “skeleton”, or instructions, of your app. This file configures most of your full-stack app for you 🤯:

app Memerator {
wasp: {
version: "^0.11.3"
},
title: "Memerator",
client: {
rootComponent: import { Layout } from "@client/Layout",
},
db: {
system: PostgreSQL,
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
auth: {
userEntity: User,
methods: {
usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
dependencies: [
("openai", "4.2.0"),
("axios", "^1.4.0"),
("react-icons", "4.10.1"),
]
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
memes Meme[]
isAdmin Boolean @default(false)
credits Int @default(2)
psl=}

entity Meme {=psl
id String @id @default(uuid())
url String
text0 String
text1 String
topics String
audience String
template Template @relation(fields: [templateId], references: [id])
templateId String
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
psl=}

entity Template {=psl
id String @id @unique
name String
url String
width Int
height Int
boxCount Int
memes Meme[]
psl=}

route HomePageRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/Home",
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup"
}

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)
  • client-side pages & routes

You might have also noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

Also, make sure you install the Wasp VS code extension so that you get nice syntax highlighting and the best overall dev experience.

Setting up the Database

We still need to get a Postgres database setup.

Usually this can be pretty annoying, but with Wasp it’s really easy.

  1. just have Docker Deskop installed and running,
  2. open up a separate terminal tab/window,
  3. cd into the Memerator directory, and then run
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 

Just leave this terminal tab, along with docker desktop, open and running in the background.

Now, in a different terminal tab, run

wasp db migrate-dev

and make sure to give your database migration a name, like init.

Environment Variables

In the root of your project, you’ll find a .env.server.example file that looks like this:

# set up your own credentials on https://imgflip.com/signup and rename this file to .env.server
# NOTE: make sure you register with Username and Password (not google)
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=

# get your api key from https://platform.openai.com/
OPENAI_API_KEY=

JWT_SECRET=asecretphraseatleastthirtytwocharacterslong

Rename this file to .env.server and follow the instructions in it to get your:

as we will need them to generate our memes 🤡

Start your App

With everything setup correctly, you should now be able to run

wasp start

When running wasp start, Wasp will install all the necessary npm packages, start our NodeJS server on port 3001, and our React client on port 3000.

Head to localhost:3000 in your browser to check it out. We should have the basis for our app that looks like this:

Image description

Generating a Meme

The boilerplate code already has the client-side form set up for generating memes based on:

  • topics
  • intended audience

This is the info we will send to the backend to call the OpenAI API using function calls. We then send this info to the imglfip.com API to generate the meme.

But the /caption_image endpoint of the imgflip API needs the meme template id. And to get that ID we first need to fetch the available meme templates from imgflip’s /get_memes endpoint

So let’s set that up now.

Server-Side Code

Create a new file in src/server/ called utils.ts:

import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';

type GenerateMemeArgs = {
text0: string;
text1: string;
templateId: string;
};

export const fetchMemeTemplates = async () => {
try {
const response = await axios.get('https://api.imgflip.com/get_memes');
return response.data.data.memes;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error fetching meme templates');
}
};

export const generateMemeImage = async (args: GenerateMemeArgs) => {
console.log('args: ', args);

try {
const data = stringify({
template_id: args.templateId,
username: process.env.IMGFLIP_USERNAME,
password: process.env.IMGFLIP_PASSWORD,
text0: args.text0,
text1: args.text1,
});

// Implement the generation of meme using the Imgflip API
const res = await axios.post('https://api.imgflip.com/caption_image', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});

const url = res.data.data.url;

console.log('generated meme url: ', url);

return url as string;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error generating meme image');
}
};

This gives us some utility functions to help us fetch all the meme templates that we can possibly generate meme images with.

Notice that the POST request to the /caption_image endpoint takes the following data:

  • our imgflip username and password
  • ID of the meme template we will use
  • the text for top of the meme, i.e. text0
  • the text for the bottom of the meme, i.e. text1

Image description

The text0 and text1 arguments will generated for us by our lovely friend, ChatGPT. But in order for GPT to do that, we have to set up its API call, too.

To do that, create a new file in src/server/ called actions.ts.

Then go back to your main.wasp config file and add the following Wasp Action at the bottom of the file:

//...

action createMeme {
fn: import { createMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

An Action is a type of Wasp Operation that changes some state on the backend. It’s essentially a NodeJS function that gets called on the server, but Wasp takes care of setting it all up for you.

This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, you just write the business logic!

Image description

If you’ve got the Wasp VS Code extension installed, you’ll see an error (above). Hover over it and click Quick Fix > create function createMeme.

This will scaffold a createMeme function (below) for you in your actions.ts file if the file exists. Pretty Cool!

import { CreateMeme } from '@wasp/actions/types'

type CreateMemeInput = void
type CreateMemeOutput = void

export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
// Implementation goes here
}

You can see that it imports the Action types for you as well.

Because we will be sending the topics array and the intended audience string for the meme from our front-end form, and in the end we will return the newly created Meme entity, that’s what we should define our types as.

Remember, the Meme entity is the database model we defined in our main.wasp config file.

Knowing that, we can change the content of actions.ts to this:

import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
// Implementation goes here
}

Before we implement the rest of the logic, let’s run through how our createMeme function should work and how our Meme will get generated:

  1. fetch the imgflip meme template we want to use
  2. send its name, the topics, and intended audience to OpenAI’s chat completions API
  3. tell OpenAI we want the result back as arguments we can pass to our next function in JSON format, i.e. OpenAI’s function calling
  4. pass those arguments to the imgflip /caption-image endpoint and get our created meme’s url
  5. save the meme url and other info into our DB as a Meme entity

With all that in mind, go ahead and entirely replace the content in our actions.ts with the completed createMeme action:

import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } from './utils.js';

import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

if (context.user.credits === 0 && !context.user.isAdmin) {
throw new HttpError(403, 'You have no credits left');
}

const topicsStr = topics.join(', ');

let templates: Template[] = await context.entities.Template.findMany({});

if (templates.length === 0) {
const memeTemplates = await fetchMemeTemplates();
templates = await Promise.all(
memeTemplates.map(async (template: any) => {
const addedTemplate = await context.entities.Template.upsert({
where: { id: template.id },
create: {
id: template.id,
name: template.name,
url: template.url,
width: template.width,
height: template.height,
boxCount: template.box_count
},
update: {}
});

return addedTemplate;
})
);
}

// filter out templates with box_count > 2
templates = templates.filter((template) => template.boxCount <= 2);
const randomTemplate = templates[Math.floor(Math.random() * templates.length)];

console.log('random template: ', randomTemplate);

const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided`;
const userPrompt = `Topics: ${topicsStr} \n Intended Audience: ${audience} \n Template: ${randomTemplate.name} \n`;

let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
try {
openAIResponse = await openai.chat.completions.create({
messages: [
{ role: 'system', content: sysPrompt },
{ role: 'user', content: userPrompt },
],
functions: [
{
name: 'generateMemeImage',
description: 'Generate meme via the imgflip API based on the given idea',
parameters: {
type: 'object',
properties: {
text0: { type: 'string', description: 'The text for the top caption of the meme' },
text1: { type: 'string', description: 'The text for the bottom caption of the meme' },
},
required: ['templateName', 'text0', 'text1'],
},
},
],
function_call: {
name: 'generateMemeImage',
},
model: 'gpt-4-0613',
});
} catch (error: any) {
console.error('Error calling openAI: ', error);
throw new HttpError(500, 'Error calling openAI');
}

console.log(openAIResponse.choices[0]);

/**
* the Function call returned by openAI looks like this:
*/
// {
// index: 0,
// message: {
// role: 'assistant',
// content: null,
// function_call: {
// name: 'generateMeme',
// arguments: '{\n' +
// ` "text0": "CSS you've been writing all day",\n` +
// ' "text1": "This looks horrible"\n' +
// '}'
// }
// },
// finish_reason: 'stop'
// }
if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');

const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
console.log('gptArgs: ', gptArgs);

const memeIdeaText0 = gptArgs.text0;
const memeIdeaText1 = gptArgs.text1;

console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);

const memeUrl = await generateMemeImage({
templateId: randomTemplate.id,
text0: memeIdeaText0,
text1: memeIdeaText1,
});

const newMeme = await context.entities.Meme.create({
data: {
text0: memeIdeaText0,
text1: memeIdeaText1,
topics: topicsStr,
audience: audience,
url: memeUrl,
template: { connect: { id: randomTemplate.id } },
user: { connect: { id: context.user.id } },
},
});

return newMeme;
};

At this point, the code above should be pretty self-explanatory, but I want to highlight a couple points:

  1. the context object is passed through to all Actions and Queries by Wasp. It contains the Prisma client with access to the DB entities you defined in your main.wasp config file.
  2. We first look for the imgflip meme templates in our DB. If none are found, we fetch them using our fetchTemplates utility function we defined earlier. Then we upsert them into our DB.
  3. There are some meme templates that take more than 2 text boxes, but for this tutorial we’re only using meme templates with 2 text inputs to make it easier.
  4. We choose a random template from this list to use as a basis for our meme (it’s actually a great way to serendipitously generate some interesting meme content).
  5. We give the OpenAI API info about the functions it can create arguments for via the functions and function_call properties, which tell it to always return JSON arguments for our function, generateMemeImage

Great! But once we start generating memes, we will need a way to display them on our front end.

So let’s now create a Wasp Query. A Query works just like an Action, except it’s only for reading data.

Go to src/server and create a new file called queries.ts.

Next, in your main.wasp file add the following code:

//...

query getAllMemes {
fn: import { getAllMemes } from "@server/queries.js",
entities: [Meme]
}

Then in your queries.ts file, add the getAllMemes function:

import HttpError from '@wasp/core/HttpError.js';

import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';

export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
const memeIdeas = await context.entities.Meme.findMany({
orderBy: { createdAt: 'desc' },
include: { template: true },
});

return memeIdeas;
};

Client-Side Code

Now that we’ve got the createMeme and getAllMemes code implemented server-side, let’s hook it up to our client.

Wasp makes it really easy to import the Operations we just created and call them on our front end.

You can do so by going to src/client/pages/Home.tsx and adding the following code to the top of the file:

//...other imports...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

// 😎 😎 😎
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience }); // <--- 😎 😎 😎
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

//...

As you can see, we’ve imported createMeme and getAllMemes (😎).

For getAllMemes, we wrap it in the useQuery hook so that we can fetch and cache the data. On the other hand, our createMeme Action gets called in handleGenerateMeme which we will call when submit our form.

Rather than adding code to the Home.tsx file piece-by-piece, here is the file with all the code to generate and display the memes. Go ahead and replace all of Home.tsx with this code and I’ll explain it in more detail below:

import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
import {
AiOutlinePlusCircle,
AiOutlineMinusCircle,
AiOutlineRobot,
} from 'react-icons/ai';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

const history = useHistory();
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

const handleDeleteMeme = async (id: string) => {
//...
};

if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Welcome to Memerator!</h1>
<p className='mb-4'>Start generating meme ideas by providing topics and intended audience.</p>
<form onSubmit={handleGenerateMeme}>
<div className='mb-4 max-w-[500px]'>
<label htmlFor='topics' className='block font-bold mb-2'>
Topics:
</label>
{topics.map((topic, index) => (
<input
key={index}
type='text'
id='topics'
value={topic}
onChange={(e) => {
const updatedTopics = [...topics];
updatedTopics[index] = e.target.value;
setTopics(updatedTopics);
}}
className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
))}
<div className='flex items-center my-2 gap-1'>
<button
type='button'
onClick={() => topics.length < 3 && setTopics([...topics, ''])}
className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
>
<AiOutlinePlusCircle /> Add Topic
</button>
{topics.length > 1 && (
<button
onClick={() => setTopics(topics.slice(0, -1))}
className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
>
<AiOutlineMinusCircle /> Remove Topic
</button>
)}
</div>
</div>
<div className='mb-4'>
<label htmlFor='audience' className='block font-bold mb-2'>
Intended Audience:
</label>
<input
type='text'
id='audience'
value={audience}
onChange={(e) => setAudience(e.target.value)}
className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
</div>
<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${
isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineRobot />
{!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
</button>
</form>

{!!memes && memes.length > 0 ? (
memes.map((memeIdea) => (
<div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
<img src={memeIdea.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>
</div>
</div>
{/* TODO: implement edit and delete meme features */}
</div>
))
) : (
<div className='flex justify-center mt-5'> :( no memes found</div>
)}
</div>
);
}

There are two things I want to point out about this code:

  1. The useQuery hook calls our getAllMemes Query when the component mounts. It also caches the result for us, as well as automatically re-fetching whenever we add a new Meme to our DB via createMeme. This means our page will reload automatically whenever a new meme is generated.
  2. The useAuth hook allows us to fetch info about our logged in user. If the user isn’t logged in, we force them to do so before they can generate a meme.

These are really cool Wasp features that make your life as a developer a lot easier 🙂

So go ahead now and try and generate a meme. Here’s the one I just generated:

Image description

Haha. Pretty good!

Now wouldn’t it be cool though if we could edit and delete our memes? And what if we could expand the set of meme templates for our generator to use? Wouldn’t that be cool, too?

Yes, it would be. So let’s do that.

Part 2.

So we’ve got ourselves a really good basis for an app at this point.

We’re using OpenAI’s function calling feature to explain a function to GPT, and get it to return results for us in a format we can use to call that function.

This allows us to be certain GPT’s result will be usable in further parts of our application and opens up the door to creating AI agents.

If you think about it, we’ve basically got ourselves a really simple Meme generating “agent”. How cool is that?!

Fetching & Updating Templates with Cron Jobs

To be able to generate our meme images via ImgFlip’s API, we have to choose and send a meme template id to the API, along with the text arguments we want to fill it in with.

For example, the Grandma Finds Internet meme template has the following id:

Image description

But the only way for us to get available meme templates from ImgFlip is to send a GET request to https://api.imgflip.com/get_memes. And according to ImgFlip, the /get-memes endpoint works like this:

Gets an array of popular memes that may be captioned with this API. The size of this array and the order of memes may change at any time. When this description was written, it returned 100 memes ordered by how many times they were captioned in the last 30 days

So it returns a list of the top 100 memes from the last 30 days. And as this is always changing, we can run a daily cron job to fetch the list and update our database with any new templates that don’t already exist in it.

We know this will work because the ImgFlip docs for the /caption-image endpoint — which we use to create a meme image — says this:

key: template_id value: A template ID as returned by the get_memes response. Any ID that was ever returned from the get_memes response should work for this parameter…

Awesome!

Defining our Daily Cron Job

Now, to create an automatically recurring cron job in Wasp is really easy.

First, go to your main.wasp file and add:

job storeMemeTemplates {
executor: PgBoss,
perform: {
fn: import { fetchAndStoreMemeTemplates } from "@server/workers.js",
},
schedule: {
// daily at 7 a.m.
cron: "0 7 * * *"
},
entities: [Template],
}

This is telling Wasp to run the fetchAndStoreMemeTemplates function every day at 7 a.m.

Next, create a new file in src/server called workers.ts and add the function:

import axios from 'axios';

export const fetchAndStoreMemeTemplates = async (_args: any, context: any) => {
console.log('.... ><><>< get meme templates cron starting ><><>< ....');

try {
const response = await axios.get('https://api.imgflip.com/get_memes');

const promises = response.data.data.memes.map((meme: any) => {
return context.entities.Template.upsert({
where: { id: meme.id },
create: {
id: meme.id,
name: meme.name,
url: meme.url,
width: meme.width,
height: meme.height,
boxCount: meme.box_count,
},
update: {},
});
});

await Promise.all(promises);
} catch (error) {
console.error('error fetching meme templates: ', error);
}
};

You can see that we send a GET request to the proper endpoint, then we loop through the array of memes it returns to us add any new templates to the database.

Notice that we use Prisma’s upsert method here. This allows us to create a new entity in the database if it doesn’t already exist. If it does, we don’t do anything, which is why update is left blank.

We use [Promise.all() to call that array of promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) correctly.

Testing

Now, assuming you’ve got your app running with wasp start, you will see the cron job run in the console every day at 7 a.m.

If you want to test that the cron job is working correctly, you could run it on a faster schedule. Let’s try that now by changing it in our main.wasp file to run every minute:

//...
schedule: {
// runs every minute.
cron: "* * * * *"
},

First, your terminal where you ran wasp start to start your app should output the following:

[Server]  🔍 Validating environment variables...
[Server] 🚀 "Username and password" auth initialized
[Server] Starting pg-boss...
[Server] pg-boss started!
[Server] Server listening on port 3001

…followed shortly after by:

[Server]  .... ><><>< get meme templates cron starting ><><>< ....

Great. We’ve got an automatically recurring cron job going.

You can check your database for saved templates by opening another terminal window and running:

wasp db studio 

Image description

Editing Memes

Unfortunately, sometimes GPT’s results have some mistakes. Or sometimes the idea is really good, but we want to further modify it to make it even better.

Well, that’s pretty simple for us to do since we can just make another call to ImgFlip’s API.

So let’s set do that by setting up a dedicated page where we:

  • fetch that specific meme based on its id
  • display a form to allow the user to edit the meme text
  • send that info to a server-side Action which calls the ImgFlip API, generates a new image URL, and updates our Meme entity in the DB.

Server-Side Code

To make sure we can fetch the individual meme we want to edit, we first need to set up a Query that does this.

Go to your main.wasp file and add this Query declaration:

query getMeme {
fn: import { getMeme } from "@server/queries.js",
entities: [Meme]
}

Now go to src/server/queries.ts and add the following function:

import type { Meme, Template } from '@wasp/entities';
import type { GetAllMemes, GetMeme } from '@wasp/queries/types';

type GetMemeArgs = { id: string };
type GetMemeResult = Meme & { template: Template };

//...

export const getMeme: GetMeme<GetMemeArgs, GetMemeResult> = async ({ id }, context) => {
if (!context.user) {
throw new HttpError(401);
}

const meme = await context.entities.Meme.findUniqueOrThrow({
where: { id: id },
include: { template: true },
});

return meme;
};

We’re just fetching the single meme based on its id from the database.

We’re also including the related meme Template so that we have access to its id as well, because we need to send this to the ImgFlip API too.

Pretty simple!

Now let’s create our editMeme action by going to our main.wasp file and adding the following Action:

//...

action editMeme {
fn: import { editMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

Next, move over to the server/actions.ts file and let’s add the following server-side function:

//... other imports
import type { EditMeme } from '@wasp/actions/types';

//... other types
type EditMemeArgs = Pick<Meme, 'id' | 'text0' | 'text1'>;

export const editMeme: EditMeme<EditMemeArgs, Meme> = async ({ id, text0, text1 }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

const meme = await context.entities.Meme.findUniqueOrThrow({
where: { id: id },
include: { template: true },
});

if (!context.user.isAdmin && meme.userId !== context.user.id) {
throw new HttpError(403, 'You are not the creator of this meme');
}

const memeUrl = await generateMemeImage({
templateId: meme.template.id,
text0: text0,
text1: text1,
});

const newMeme = await context.entities.Meme.update({
where: { id: id },
data: {
text0: text0,
text1: text1,
url: memeUrl,
},
});

return newMeme;
};

As you can see, this function expects the id of the already existing meme, along with the new text boxes. That’s because we’re letting the user manually input/edit the text that GPT generated, rather than making another request the the OpenAI API.

Next, we look for that specific meme in our database, and if we don’t find it we throw an error (findUniqueOrThrow).

We check to make sure that that meme belongs to the user that is currently making the request, because we don’t want a different user to edit a meme that doesn’t belong to them.

Then we send the template id of that meme along with the new text to our previously created generateMemeImage function. This function calls the ImgFlip API and returns the url of the newly created meme image.

We then update the database to save the new URL to our Meme.

Awesome!

Client-Side Code

Let’s start by adding a new route and page to our main.wasp file:

//...

route EditMemeRoute { path: "/meme/:id", to: EditMemePage }
page EditMemePage {
component: import { EditMemePage } from "@client/pages/EditMemePage",
authRequired: true
}

There are two important things to notice:

  1. the path includes the :id parameter, which means we can access page for any meme in our database by going to, e.g. memerator.com/meme/5
  2. by using the authRequired option, we tell Wasp to automatically block this page from unauthorized users. Nice!

Now, create this page by adding a new file called EditMemePage.tsx to src/client/pages. Add the following code:

import { useState, useEffect, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import editMeme from '@wasp/actions/editMeme';
import getMeme from '@wasp/queries/getMeme';
import { useParams } from 'react-router-dom';
import { AiOutlineEdit } from 'react-icons/ai';

export function EditMemePage() {
// http://localhost:3000/meme/573f283c-24e2-4c45-b6b9-543d0b7cc0c7
const { id } = useParams<{ id: string }>();

const [text0, setText0] = useState('');
const [text1, setText1] = useState('');
const [isLoading, setIsLoading] = useState(false);

const { data: meme, isLoading: isMemeLoading, error: memeError } = useQuery(getMeme, { id: id });

useEffect(() => {
if (meme) {
setText0(meme.text0);
setText1(meme.text1);
}
}, [meme]);

const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
try {
setIsLoading(true);
await editMeme({ id, text0, text1 });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsLoading(false);
}
};

if (isMemeLoading) return 'Loading...';
if (memeError) return 'Error: ' + memeError.message;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Edit Meme</h1>
<form onSubmit={handleSubmit}>
<div className='flex gap-2 items-end'>
<div className='mb-2'>
<label htmlFor='text0' className='block font-bold mb-2'>
Text 0:
</label>
<textarea
id='text0'
value={text0}
onChange={(e) => setText0(e.target.value)}
className='border rounded px-2 py-1'
/>
</div>
<div className='mb-2'>
<label htmlFor='text1' className='block font-bold mb-2'>
Text 1:
</label>

<div className='flex items-center mb-2'>
<textarea
id='text1'
value={text1}
onChange={(e) => setText1(e.target.value)}
className='border rounded px-2 py-1'
/>
</div>
</div>
</div>

<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm py-1 px-2 rounded ${
isLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineEdit />
{!isLoading ? 'Save Meme' : 'Saving...'}
</button>
</form>
{!!meme && (
<div className='mt-4 mb-2 bg-gray-100 rounded-lg p-4'>
<img src={meme.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{meme.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{meme.audience}</span>
</div>
<div>
<span className='text-sm text-gray-700'>ImgFlip Template: </span>
<span className='text-sm italic text-gray-500'>{meme.template.name}</span>
</div>
</div>
</div>
)}
</div>
);
}

Some things to notice here are:

  1. because we’re using dynamic routes (/meme/:id), we pull the URL paramater id from the url with useParams hook.
  2. we then pass that id within the getMemes Query to fetch that specific meme to edit: useQuery(getMeme, { id: id })
    1. remember, our server-side action depends on this id in order to fetch the meme from our database

The rest of the page is just our form for calling the editMeme Action, as well as displaying the meme we want to edit.

That’s great!

Now that we have that EditMemePage, we need a way to navigate to it from the home page.

To do that, go back to the Home.tsx file, add the following imports at the top, and find the comment that says {/* TODO: implement edit and delete meme features */} and replace it with the following code:

import { Link } from '@wasp/router';
import { AiOutlineEdit } from 'react-icons/ai';

//...

{user && (user.isAdmin || user.id === memeIdea.userId) && (
<div className='flex items-center mt-2'>
<Link key={memeIdea.id} params={{ id: memeIdea.id }} to={`/meme/:id`}>
<button className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'>
<AiOutlineEdit />
Edit Meme
</button>
</Link>
{/* TODO: add delete meme functionality */}
</div>
)}

What’s really cool about this, is that Wasp’s Link component will give you type-safe routes, by making sure you’re following the pattern you defined in your main.wasp file.

And with that, so long as the authenticated user was the creator of the meme (or is an admin), the Edit Meme button will show up and direct the user to the EditMemePage

Give it a try now. It should look like this:

Deleting Memes

Ok. When I initially started writing this tutorial, I thought I’d also explain how to add delete meme functionality to the app as well.

But seeing as we’ve gotten this far, and as the entire two-part tutorial is pretty long, I figured you should be able to implement yourself by this point.

So I’ll leave you guide as to how to implement it yourself. Think of it as a bit of homework:

  1. define the deleteMeme Action in your main.wasp file
  2. export the async function from the actions.ts file
  3. import the Action in your client-side code
  4. create a button which takes the meme’s id as an argument in your deleteMeme Action.

If you get stuck, you can use the editMeme section as a guide. Or you can check out the finished app’s GitHub repo for the completed code!

Conclusion

There you have it! Your own instant meme generator 🤖😆

BTW, If you found this useful, please show us your support by giving us a star on GitHub! It will help us continue to make more stuff just like it.

https://res.cloudinary.com/practicaldev/image/fetch/s--tnDxibZC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--OCpry2p9--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_66%252Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bky8z46ii7ayejprrqw3.gif

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/10/04/contributing-open-source-land-a-job.html b/blog/2023/10/04/contributing-open-source-land-a-job.html index 190fb30470..bc46f360a7 100644 --- a/blog/2023/10/04/contributing-open-source-land-a-job.html +++ b/blog/2023/10/04/contributing-open-source-land-a-job.html @@ -19,15 +19,15 @@ - - + +
-

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

· 10 min read
Vinny

TL;DR

How to Open-Source +

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

· 10 min read
Vinny

TL;DR

How to Open-Source In this article, we’re going to see how open-source can change your career for the better and get you out of the Skill Paradox — a point in which the skills you need to land a job are generally acquired after you get a job.

Besides that, we’ll check how you can start contributing to different open-source projects and get on the hype train of Hacktoberfest while also learning some important topics on handling feedbacks and showcasing your contributions.

1. Introduction

Are you a beginner developer that lacks certain skills needed to land a job? But you feel that you could only gain those skills on the job itself? If you answered “yes”, then you’re stuck in situation that I would call as the “skill paradox where you need skills to get a job, but those skills are the ones you would get if you had a job. It can generate a lot of stress and frustration when you start to realize that some skills cannot be obtained while working only on side hustles and therefore, you cannot learn only by yourself, but they’re generally required for job positions.

Collaboration and teamwork, learning how to code review (giving and receiving feedback), and getting started with bigger and existing codebases are things that cannot be taught while you work on some little projects. While, of course, you can learn those skills while getting a job in tech, sometimes those skills are necessary for you to get a job, making you stay in some kind of limbo where you need some skills to get a job, and those skills are precisely the ones you would get after the job.

In those cases, there’s still a way out of the limbo: you can contribute to open-source communities. Besides the value you are generating for the whole ecosystem, this can be an amazing selling point for your career and, since Hacktoberfest is already around the corner, will be a great way to win a t-shirt or plant a tree too!

Now, let’s begin by teaching you how to actually do this.

2. First steps on Open-Source Contribution

2.1. Finding a project

First of all, we need to choose a project. If you’re a beginner, you’re probably looking for projects that have a few characteristics:

  • It’s actively maintained.
  • Has an open-source license that we can modify and use freely.
  • It’s not insanely big (since these projects can have some really hard things to accomplish before submitting something).
  • It must have good documentation on how to contribute.
  • It must have well-characterized issues in order for you to search for something (in the case that you haven’t found the problem itself).

If you have matches in all of these points (or at least three of them), you’re good to go!

Throughout this article, I’m going to use our own repo, Wasp Full-stack Framework, since it gathers all the characteristics necessary for a good open-source repository.

So, let me show you how to find all these characteristics:

  • It’s actively maintained and the owners of the repo reply and care for the issues!
    • In the case of Wasp’s repo, the last commit was 13 hours ago, so, there’s definitely signs of life here!

Last commit

  • It’s not insanely big → Comparing an exaggerated example with the Linux repo (if you check it, you’ll see that all pull requests there usually take a lot of time to be merged since the project is so big)

Linux repo

  • It’s good to have a documentation on how to contribute
    • Searching for the docs, I found a file called CONTRIBUTING.md (which is a common name standard for contribution guidelines) and when we open it up:

Contributing guidelines

We have a whole documentation on how to start with things! Awesome!

  • It’s good to have well characterized issues in order for you to search for something

Issues

Searching for the issues, we can easily see that they’re all labeled and that will help us A TON!

2.2. Searching for Issues

Great! Now that we have already chosen where we are going to contribute, let’s dive into the issues and search for something we want to do!

When searching for issues, the labels do us a great favor by already explicitly identifying all issues that can be good for newcomers! If you’re a beginner, good first issues and documentation are excellent labels for you to search for!

Good labels to search for

Issues on the repo labeled

Opening the first issue, we can see that someone already manifested interest on it! So, since someone has already manifested interest in that one, let’s search for another one!

The first issue

Finding another issue — it doesn’t look like anyone is working on the one below, so we can take it ourselves!

Finding another issue

By the way, it's of absolute importance that, when you find an issue, you comment and set yourself as assignee in order to let other people know that you're going to take the task at hand!

Communicating

In this case, GitHub is a great platform for us to discuss, but sometimes authors can be hard to find. In these cases, search for a link or a way to contact them directly (in the case of Wasp, they have a Discord server, for example). Communicating your way through is really important to get things sorted out, and if you’re unsure of how to communicate well with people, you can read this other article here and start to get the hang of it!

3. Guidelines for Contributing to Open-Source Projects

3.1. Reading the guidelines and writing some code

Now that we have selected a repo, an issue to work on and communicated with the authors, it’s time to check the guidelines for making Pull Requests (if you don’t know what this means, it’s basically a request to merge your modifications to the codebase, you can check some more basic git terms here too). Sometimes, these guidelines are WAY too hard and sometimes they don’t even exist (that’s an awesome first issue actually), anyways look it up and see if you find something!

You can check Wasp’s contributing guidelines here if you want to read it yourself! After reading it, it’s time to code the solution and get along with it.

Since the intent of this article is not to actually show the solving per se, I’ll skip this part and keep talking about the process itself.

3.2 Handling Code Reviews and Feedback

It’s not rare that when we code things up (especially in open-source projects), there will be some problems. Code reviews and feedback are an amazing way for us to get the bigger picture and improve our code quality, so let’s check on how to properly read and answer code reviews and feedback.

We’re generally used to receiving criticism in a harsh way, so, when someone approaches you with feedbacks, we generally move into our defense zone. Unfortunately, these cases can teach you the wrong things as it’s generally a good way to think of feedbacks as gifts! Someone spent some time writing (or speaking) things in order for you get even better on what you’re trying to accomplish.

This does not mean that all feedback is well-made or that people will always provide great feedback. Sometimes, people can be harsh. However, as you receive more and more feedbacks, you will develop a sense of which feedbacks are genuinely meant to help you improve and which are simply baseless criticism. It is crucial to be open to receiving constructive feedbacks and not take them personally.

Let’s see an example of code review and feedback here:

Code Review Example

This is great feedback! It expresses the author’s opinion without being harsh and also suggests what to make in order to be perfect! The best way to answer this is simply:

  • Thanking for the feedback
  • Saying your opinion (agree or disagree) when it makes sense
  • Work on it!

Showcasing Contributions

After all that work, it’s time for us to showcase our contributions! Document it all. GitHub (or other git platforms), personal portfolio sites, LinkedIn, and other means of reaching people have become as important as resumes nowadays, so it’s really nice to have some statistics and data to display on:

  • What open-source projects have you worked on? Try to think of this as writing a story. First, start by giving the initial context of the project and how it’s revelatory.
  • How you contributed: Then, give the context of what you made, documentation, code, and problems you solved in general. Don’t forget to not focus a lot on the technical side since the person who could be reading this may not be technical.
  • How big was the impact? Talk about how this affected the ecosystem; it can be as big or as small as you like. Never neglect the impact that changing documentation can have (remember that for us, programmers, the documentation is our source of truth, and fixes there are greatly appreciated).

Don’t forget to utilize the opportunity to engage with other developers and communities, make it so in order to get new connections and even greater opportunities later on!

Now that the theory is set, let’s check a few examples on how I would showcase a few of my contributions:

Case 1 - A big contribution

One of the ways to describe a big contribution is like this:

I made a few big contributions to a project called Coolify, which was an open-source Heroku alternative. I refactored a lot of the UI, making it cleaner and more consistent throughout the application. Currently, more than 9000 instances are installed, and the UI affects all of them! You can check out the contributions here.

Of course, you can make this text as long or as short as you want, entering more detail about how this contribution was made and what exactly you did, but for this article, this is enough for you to get a general idea.

Case 2- A small contribution

One way to describe a small contribution is like this:

I made a small change to the new documentation for Sequelize! I was just scrolling through the documentation and found this mistake that could lead others to weird debugging sessions, so as soon as I found it, I submitted a PR for them! You can check out the contribution here!

Conclusion

So, a lot was said, let’s make a quick recap on how to do contributions and how to showcase them:

  • First of all, find a repo! If you don’t have any in mind, there loads of lists (like this one) that recommend some repos for you to take a look
  • Search for an issue that is not being made and you can work on it, if you’re beginner, check for documentation and good first issue labels
  • Comment and communicate that you’re going to fix the issue - take the opportunity to talk and get to know other developers
  • Code, get you PR reviewed and ready to merge after the feedbacks
  • Merge and showcase your contributions, showing that they are your way out of the Skill Paradox

How to Open-Source

The above steps can give you a really powerful experience in software engineering (which usually happens only when you’re already hired by a company). This is an awesome way to get some recognition while improving the open-source community — giving back to other developers and getting yourself out of the Skill Paradox!

And you? Have you contributed to open-source? Let me know in the comments below, and let’s share some experiences!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/10/12/on-importance-of-naming-in-programming.html b/blog/2023/10/12/on-importance-of-naming-in-programming.html index fc9a7cf113..6316aa9e3a 100644 --- a/blog/2023/10/12/on-importance-of-naming-in-programming.html +++ b/blog/2023/10/12/on-importance-of-naming-in-programming.html @@ -19,16 +19,16 @@ - - + +
-

On the Importance of Naming in Programming

· 12 min read
Martin Sosic

In stories, you will often find the motif of a powerful demon that can be controlled only by knowing its true name. Once the hero finds out that name, through cunning dialogue or by investigating ancient tomes, they can turn things around and banish the demon!

I firmly believe writing code is not much different: through finding good names for functions, variables, and other constructs, we truly recognize the essence of the problem we are solving. The consequence of clarity gained is not just good names but also cleaner code and improved architecture.

The power of correct naming in programming

I would go as far as to say that 90% of writing clean code is “just” naming things correctly. +

On the Importance of Naming in Programming

· 12 min read
Martin Sosic

In stories, you will often find the motif of a powerful demon that can be controlled only by knowing its true name. Once the hero finds out that name, through cunning dialogue or by investigating ancient tomes, they can turn things around and banish the demon!

I firmly believe writing code is not much different: through finding good names for functions, variables, and other constructs, we truly recognize the essence of the problem we are solving. The consequence of clarity gained is not just good names but also cleaner code and improved architecture.

The power of correct naming in programming

I would go as far as to say that 90% of writing clean code is “just” naming things correctly. Sounds simple, but it is really not!

Let’s take a look at a couple of examples.

Example #1

// Given first and last name of a person, returns the
// demographic statistics for all matching people.
async function demo (a, b) {
const c = await users(a, b);
return [
avg(c.map(a => a.info[0])),
median(c.map(a => a.info[1]))
];
}

What is wrong with this code?

  1. The name of the function demo is very vague: it could stand for “demolish”, or as in “giving a demo/presentation”, … .
  2. Names a, b, and c are completely uninformative.
  3. a is reused in lambda inside the map, shadowing the a that is a function argument, confusing the reader and making it easier to make a mistake when modifying the code in the future and reference the wrong variable.
  4. The returned object doesn’t have any info about what it contains, instead, you need to be careful about the order of its elements when using it later.
  5. The name of the field .info in the result of a call to users() function gives us no information as to what it contains, which is made further worse by its elements being accessed by their position, also hiding any information about them and making our code prone to silently work wrong if their ordering changes.

Let’s fix it:

async function fetchDemographicStatsForFirstAndLastName (
firstName, lastName
) {
const users = await fetchUsersByFirstAndLastName(
firstName, lastName
);
return {
averageAge: avg(users.map(u => u.stats.age)),
medianSalary: median(users.map(u => u.stats.salary))
};
}

What did we do?

  1. The name of the function now exactly reflects what it does, no more no less. fetch in the name even indicates it does some IO (input/output, in this case fetching from the database), which can be good to know since IO is relatively slow/expensive compared to pure code.
  2. We made other names informative enough: not too much, not too little.
    • Notice how we used the name users for fetched users, and not something longer like usersWithSpecifiedFirstAndLastName or fetchedUsers: there is no need for a longer name, as this variable is very local, short-lived, and there is enough context around it to make it clear what it is about.
    • Inside lambda, we went with a single-letter name, u, which might seem like bad practice. But, here, it is perfect: this variable is extremely short-lived, and it is clear from context what it stands for. Also, we picked specifically the letter u for a reason, as it is the first letter of user, therefore making that connection obvious.
  3. We named values in the object that we return: averageAge and medianSalary. Now any code that will use our function won’t need to rely on the ordering of items in the result, and also will be easy and informative to read.

Finally, notice how there is no comment above the function anymore. The thing is, the comment is not needed anymore: it is all clear from the function name and arguments!

Example #2

// Find a free machine and use it, or create a new machine
// if needed. Then on that machine, set up the new worker
// with the given Docker image and setup cmd. Finally,
// start executing a job on that worker and return its id.
async function getJobId (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

In this example, we are ignoring the implementation details and will focus just on getting the name and arguments right.

What is wrong with this code?

  1. The function name is hiding a lot of details about what it is doing. It doesn’t mention at all that we have to procure the machine or set up the worker, or that function will result in the creation of a job that will continue executing somewhere in the background. Instead, it gives a feeling that we are doing something simple, due to the verb get: we are just obtaining an id of an already existing job. Imagine seeing a call to this function somewhere in the code: getJobId(...)you are not expecting it to take long or do all of the stuff that it really does, which is bad.

Ok, this sounds easy to fix, let’s give it a better name!

async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

Uff, that is one long and complicated name. But the truth is, that we can’t really make it shorter without losing valuable information about what this function does and what we can expect from it. Therefore, we are stuck, we can’t find a better name! What now?

The thing is, you can't give a good name if you don't have clean code behind it. So a bad name is not just a naming mishap, but often also an indicator of problematic code behind it, a failure in design. Code so problematic, that you don’t even know what to name it → there is no straightforward name to give to it, because it is not a straightforward code!

Bad name is hiding bad code

In our case, the problem is that this function is trying to do too much at once. A long name and many arguments are indicators of this, although these can be okay in some situations. Stronger indicators are the usage of words “and” and “then” in the name, as well as argument names that can be grouped by prefixes (machine, worker).

The solution here is to clean up the code by breaking down the function into multiple smaller functions:

async function procureFreeMachine (type, region) { ... }
async function setUpDockerWorker (machineId, dockerImage, setupCmd) { ... }
async function startExecutingJob (workerId, jobDescription) { ... }

What is a good name?

But let’s take a step back - what is a bad name, and what is a good name? What does that mean, how do we recognize them?

Good name doesn’t misdirect, doesn’t omit, and doesn’t assume.

A good name should give you a good idea about what the variable contains or function does. A good name will tell you all there is to know or will tell you enough to know where to look next. It will not let you guess, or wonder. It will not misguide you. A good name is obvious, and expected. It is consistent. Not overly creative. It will not assume context or knowledge that the reader is not likely to have.

Also, context is king: you can’t evaluate the name without the context in which it is read. verifyOrganizationChainCredentials could be a terrible name or a great name. a could be a great name or a terrible name. It depends on the story, the surroundings, on the problem the code is solving. Names tell a story, and they need to fit together like a story.

Examples of famous bad names

  • JavaScript
    • I was the victim of this bad naming myself: my parents bought me a book about JavaScript while I wanted to learn Java.
  • HTTP Authorization header
  • Wasp-lang:
    • This one is my fault: Wasp is a full-stack JS web framework that uses a custom config language as only a small part of its codebase, but I put -lang in the name and scared a lot of people away because they thought it was a whole new general programming language!

How to come up with a good name

Don’t give a name, find it

The best advice is maybe not to give a name, but instead to find out a name. You shouldn’t be making up an original name, as if you are naming a pet or a child; you are instead looking for the essence of the thing you are naming, and the name should present itself based on it. If you don’t like the name you discovered, it means you don’t like the thing you are naming, and you should change that thing by improving the design of your code (as we did in the example #2).

You shouldn't name your variables the same way you name your pets, and vice versa

Things to look out for when figuring out a name

  1. First, make sure it is not a bad name :). Remember: don’t misdirect, don’t omit, don’t assume.
  2. Make it reflect what it represents. Find the essence of it, capture it in the name. Name is still ugly? Improve the code. You have also other things to help you here → type signature, and comments. But those come secondary.
  3. Make it play nicely with the other names around it. It should have a clear relation to them - be in the same “world”. It should be similar to similar stuff, opposite to opposite stuff. It should make a story together with other names around it. It should take into account the context it is in.
  4. Length follows the scope. In general, the shorter-lived the name is, and the smaller its scope is, the shorter the name can/should be, and vice versa. This is why it can be ok to use one-letter variables in short lambda functions. If not sure, go for the longer name.
  5. Stick to the terminology you use in the codebase. If you so far used the term server, don’t for no reason start using the term backend instead. Also, if you use server as a term, you likely shouldn't go with frontend: instead, you will likely want to use client, which is a term more closely related to the server.
  6. Stick to the conventions you use in the codebase. Examples of some of the conventions that I often use in my codebases:
    • prefix is when the variable is Bool (e.g. isAuthEnabled)
    • prefix ensure for the functions that are idempotent, that will do something (e.g allocate a resource) only if it hasn’t been set up so far (e.g. ensureServerIsRunning).

The simple technique for figuring out a name every time

If you are ever having trouble coming up with a name, do the following:

  1. Write a comment above the function/variable where you describe what it is, in human language, as if you were describing it to your colleague. It might be one sentence or multiple sentences. This is the essence of what your function/variable does, what it is.
  2. Now, you take the role of the sculptor, and you chisel at and shape that description of your function/variable until you get a name, by taking pieces of it away. You stop when you feel that one more hit of your imagined chisel at it would take too much away.
  3. Is your name still too complex/confusing? If that is so, that means that the code behind is too complex, and should be reorganized! Go refactor it.
  4. Ok, all done → you have a nice name!
  5. That comment above the function/variable? Remove everything from it that is now captured in the code (name + arguments + type signature). If you can remove the whole comment, great. Sometimes you can’t, because some stuff can’t be captured in the code (e.g. certain assumptions, explanations, examples, …), and that is also okay. But don’t repeat in the comment what you can say in the code instead. Comments are a necessary evil and are here to capture knowledge that you can’t capture in your names and/or types.

Don’t get overly stuck on always figuring out the perfect name at the get-go → it is okay to do multiple iterations of your code, with both your code and name improving with each iteration.

Reviewing code with naming in mind

Once you start thinking a lot about naming, you will see how it will change your code review process: focus shifts from looking at implementation details to looking at names first.

When I am doing a code review, there is one predominant thought I will be thinking about: “Is this name clear?”. From there, the whole review evolves and results in clean code.

Inspecting a name is a single point of pressure, that untangles the whole mess behind it. Search for bad names, and you will sooner or later uncover the bad code if there is some.

Further reading

If you haven’t yet read it, I would recommend reading the book Clean Code by Robert Martin. It has a great chapter on naming and also goes much further on how to write code that you and others will enjoy reading and maintaining.

Also, A popular joke about naming being hard.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/10/13/wasp-launch-week-four.html b/blog/2023/10/13/wasp-launch-week-four.html index 2734630c62..baeed44efb 100644 --- a/blog/2023/10/13/wasp-launch-week-four.html +++ b/blog/2023/10/13/wasp-launch-week-four.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #4: Waspolution

· 5 min read
Matija Sosic

Launch Week 4 is coming

We're back! Beginning of the October was both the craziest and busiest time we've ever had at Wasp - we ended up on GitHub Trending (almost at 7,000 stars - thank you🙏), MAGE (our GPT web app generator) exploded with 20,000 apps created and more people than ever used Wasp!

Crazily enough, we've even had a first startup project made in Wasp that has been acquired (GPT-powered, of course)! 💸🐝💸

To top it all off, we have a new launch week incoming that brings a ton of new exciting product updates! If this is your first rodeo, check out our previous launches:

What's this launch all about?

As you might have noticed, each of our launches comes with a specific theme. We've come a long way since our first launch week, Beta release, which moved Wasp from a prototype to a real, working framework. In the previous two launch weeks we've added plenty of new features and unlocked functionalities you couldn't have used before (e.g. email sending, async jobs, ...).

This time we kept introducing new features, but we also realised there are many opportunities to make developers' lives even easier. That's why the theme of this launch week stems from "Evolution" - Wasp is now well set on its way, lies on the solid foundations with a strong community behind it and keeps naturally evolving!

Growing up
Wasp from this launch onwards.

Enough chit-chat - let's see what will go down next week! We'll present a new feature (or more of them) every day. To stay in the loop follow us on Twitter/X (@WaspLang) and join our community on Wasp Discord!

Launch party 🚀🎉

launch event 3 - screenshot
A bit of the atmosphere from LW3 launch party!

As it is a tradition by now, we'll kick things off with a launch party on our Discord! You will be able to meet the team and be the first one to learn about the new features we'll be revealing for the rest of the week. We'll also answer community questions, discuss plans for the future, and of course, hand out some sweet swag (finally get your hands on that Da Boi plushie)!

The party starts at 11 am EDT / 5 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

Monday: I am Speed 🚄

Why waste time
We think the same, but about keystrokes.

We all know that developer productivity is a hot topic these days. At the end of the day, why waste time use many keystroke when few do trick?

That's exactly what we will feature on Monday! Wasp is already famous for its brevity and prototyping speed, which is powered by its high-level configuration language, but we found a way to make things even simpler!

When: Monday, October 16 2023

Tuesday: Safety first 👷

Realtime

In every industry they have strict safety protocols - we believe programming should be no different! Especially when it comes to types - imagine if you had a piece of data running around your application, without even knowing what it looks like!? No sir, not under my watch ⬇️⌚️.

When: Tuesday, October 17 2023

Wednesday: Wasp x AI x ...base 🤖⚡️

Power Rangers

The best things happen when you combine multiple amazing things together - and that's exactly what we did! I don't want to spoil too much, but let's just say it has become much easier to do a certain similarity search with Wasp 😉.

I don't want to overhype it, but it might be one of the coolest things you've seen so far - see you on Wednesday!

When: Wednesday, October 18 2023

Thursday: A glimpse into the future 🛸

World if everyone used Wasp for web development

Although there is a plenty of work to refine the existing features and polish the overal developer experience, we still always have our eyes on the future and take time to experiment. This is what we will present here - a really cool feature that is possible due to the Wasp's unique approach, that will illuminate a lot posibilities for the future!

When: Thursday, October 19 2023

Friday: Polish 💅

Wax on, Wax off

Sometimes, the best thing you can do is take care of what you already have! As we mentioned in the intro, Wasp is becoming all about DX, feature completeness and elegance of use. And this is what we will demonstrate today!

When: Friday, October 20 2023

Monday: SaaS-a-thon!

Hacking

As the ancient scrolls say, every launch week must end with a hackathon, and this is no exception! We'll share more details soon, but as the title says, we'll equip you as well as possible to create a SaaS of your dreams in no time!

When: Monday, October 23 2023

Recap

  • We are kicking off Launch Week #4 on Mon, Oct 16, at 11am EDT / 5pm CET - make sure to register for the event!
  • Launch Week #4 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a SaaS-a-thon - get your keyboards warmed up and ready to roll!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #4: Waspolution

· 5 min read
Matija Sosic

Launch Week 4 is coming

We're back! Beginning of the October was both the craziest and busiest time we've ever had at Wasp - we ended up on GitHub Trending (almost at 7,000 stars - thank you🙏), MAGE (our GPT web app generator) exploded with 20,000 apps created and more people than ever used Wasp!

Crazily enough, we've even had a first startup project made in Wasp that has been acquired (GPT-powered, of course)! 💸🐝💸

To top it all off, we have a new launch week incoming that brings a ton of new exciting product updates! If this is your first rodeo, check out our previous launches:

What's this launch all about?

As you might have noticed, each of our launches comes with a specific theme. We've come a long way since our first launch week, Beta release, which moved Wasp from a prototype to a real, working framework. In the previous two launch weeks we've added plenty of new features and unlocked functionalities you couldn't have used before (e.g. email sending, async jobs, ...).

This time we kept introducing new features, but we also realised there are many opportunities to make developers' lives even easier. That's why the theme of this launch week stems from "Evolution" - Wasp is now well set on its way, lies on the solid foundations with a strong community behind it and keeps naturally evolving!

Growing up
Wasp from this launch onwards.

Enough chit-chat - let's see what will go down next week! We'll present a new feature (or more of them) every day. To stay in the loop follow us on Twitter/X (@WaspLang) and join our community on Wasp Discord!

Launch party 🚀🎉

launch event 3 - screenshot
A bit of the atmosphere from LW3 launch party!

As it is a tradition by now, we'll kick things off with a launch party on our Discord! You will be able to meet the team and be the first one to learn about the new features we'll be revealing for the rest of the week. We'll also answer community questions, discuss plans for the future, and of course, hand out some sweet swag (finally get your hands on that Da Boi plushie)!

The party starts at 11 am EDT / 5 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

Monday: I am Speed 🚄

Why waste time
We think the same, but about keystrokes.

We all know that developer productivity is a hot topic these days. At the end of the day, why waste time use many keystroke when few do trick?

That's exactly what we will feature on Monday! Wasp is already famous for its brevity and prototyping speed, which is powered by its high-level configuration language, but we found a way to make things even simpler!

When: Monday, October 16 2023

Tuesday: Safety first 👷

Realtime

In every industry they have strict safety protocols - we believe programming should be no different! Especially when it comes to types - imagine if you had a piece of data running around your application, without even knowing what it looks like!? No sir, not under my watch ⬇️⌚️.

When: Tuesday, October 17 2023

Wednesday: Wasp x AI x ...base 🤖⚡️

Power Rangers

The best things happen when you combine multiple amazing things together - and that's exactly what we did! I don't want to spoil too much, but let's just say it has become much easier to do a certain similarity search with Wasp 😉.

I don't want to overhype it, but it might be one of the coolest things you've seen so far - see you on Wednesday!

When: Wednesday, October 18 2023

Thursday: A glimpse into the future 🛸

World if everyone used Wasp for web development

Although there is a plenty of work to refine the existing features and polish the overal developer experience, we still always have our eyes on the future and take time to experiment. This is what we will present here - a really cool feature that is possible due to the Wasp's unique approach, that will illuminate a lot posibilities for the future!

When: Thursday, October 19 2023

Friday: Polish 💅

Wax on, Wax off

Sometimes, the best thing you can do is take care of what you already have! As we mentioned in the intro, Wasp is becoming all about DX, feature completeness and elegance of use. And this is what we will demonstrate today!

When: Friday, October 20 2023

Monday: SaaS-a-thon!

Hacking

As the ancient scrolls say, every launch week must end with a hackathon, and this is no exception! We'll share more details soon, but as the title says, we'll equip you as well as possible to create a SaaS of your dreams in no time!

When: Monday, October 23 2023

Recap

  • We are kicking off Launch Week #4 on Mon, Oct 16, at 11am EDT / 5pm CET - make sure to register for the event!
  • Launch Week #4 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a SaaS-a-thon - get your keyboards warmed up and ready to roll!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/11/21/guide-windows-development-wasp-wsl.html b/blog/2023/11/21/guide-windows-development-wasp-wsl.html index 870ca4e255..d6a504f1b0 100644 --- a/blog/2023/11/21/guide-windows-development-wasp-wsl.html +++ b/blog/2023/11/21/guide-windows-development-wasp-wsl.html @@ -19,14 +19,14 @@ - - + +
-

A Guide to Windows Development with Wasp & WSL

· 10 min read
Boris Martinović

WSL Guide Banner

If you are having a hard time with Wasp development on Windows, don't be afraid! We will go through all necessary steps to set up your dev environment and get you started with Wasp development in Windows in no time.

What is WSL and why should I be interested in it?

Windows Subsystem for Linux (or WSL) lets developers run a fully functional and native GNU/Linux environment directly on Windows. In other words, we can run Linux directly without using a virtual machine or dual-booting the system.

The first cool thing about it is that WSL allows you to never switch OS’s, but still have the best of both worlds inside your OS. +

A Guide to Windows Development with Wasp & WSL

· 10 min read
Boris Martinović

WSL Guide Banner

If you are having a hard time with Wasp development on Windows, don't be afraid! We will go through all necessary steps to set up your dev environment and get you started with Wasp development in Windows in no time.

What is WSL and why should I be interested in it?

Windows Subsystem for Linux (or WSL) lets developers run a fully functional and native GNU/Linux environment directly on Windows. In other words, we can run Linux directly without using a virtual machine or dual-booting the system.

The first cool thing about it is that WSL allows you to never switch OS’s, but still have the best of both worlds inside your OS. What does that mean for us regular users? When you look at the way WSL works in practice, it can be considered a Windows feature that runs a Linux OS directly inside Windows 10 or 11, with a fully functional Linux file system, Linux command line tools, and Linux GUI apps (really cool, btw). Besides that, it uses much fewer resources for running when compared to a virtual machine and also doesn’t require a separate tool for creating and managing those virtual machines.

WSL is mainly catered to developers, so this article will be focused on developer usage and how to set up a fully working dev environment with VS Code. Inside this article, we’ll go through some of the cool features and how they can be used in practice. Plus, the best way to understand new things is to actually start using them.

Installing WSL on the Windows operating system

In order to install WSL on your Windows, first enable Hyper-V architecture is Microsoft’s hardware virtualization solution. To install it, right-click on the Windows Terminal/Powershell and open it in Administrator mode.

Image description

Then, run the following command:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

That will ensure that you have all the prerequisites for the installation. Then, open the Powershell (best done in Windows Terminal) in the Administrator mode. Then, run

wsl —install

There is a plethora of Linux distributions to be installed, but Ubuntu is the one installed by default. This guide will feature many console commands, but most of them will be a copy-paste process.

If you have installed Docker before, there is a decent chance that you have WSL 2 installed on your system already. In that case, you will get a prompt to install the distribution of your choice. Since this tutorial will be using Ubuntu, I suggest running.

 wsl --install -d Ubuntu

After installing Ubuntu (or another distro of your choice), you will enter your Linux OS and be prompted with a welcome screen. There, you will enter some basic info. First, you will enter your username and after that your password. Both of those will be Linux-specific, so you don’t necessarily have to repeat your Windows credentials. After we’ve done this, the installation part is over! You have successfully installed Ubuntu on your Windows machine! It still feels weird to say this, right?

Cool WSL featues to help you along the way

But before we get down to our dev environment setup, I want to show you a couple of cool tricks that will make your life easier and help you understand why WSL is actually a game-changer for Windows users.

The first cool thing with WSL is that you don’t have to give up the current way of managing files through Windows Explorer. In your sidebar in Windows Explorer, you can find the Linux option now right under the network tab.

Image description

From there, you can access and manage your Linux OS’s file system directly from the Windows Explorer. What is really cool with this feature is that you can basically copy, paste, and move files between different operating systems without any issues, which opens up a whole world of possibilities. Effectively, you don’t have to change much in your workflow with files and you can move many projects and files from one OS to another effortlessly. If you download an image for your web app on your Windows browser, just copy and paste it to your Linux OS.

Image description

Another very important thing, which we will use in our example is WSL2 virtual routes. As you now have OS inside your OS, they have a way of communicating. When you want to access your Linux OS’s network (for example, when you want to access your web app running locally in Linux), you can use ${PC-name}.local. For me, since my PC name is Boris-PC, my network address is boris-pc.local. That way you don’t have to remember different IP addresses, which is really cool. If you want your address for whatever reason, you can go to your Linux distro’s terminal, and type ipconfig. Then, you can see your Windows IP and Linux’s IP address. With that, you can communicate with both operating systems without friction.

Image description

The final cool thing I want to highlight is Linux GUI apps. It is a very cool feature that helps make WSL a more attractive proposal for regular users as well. You can install any app you want on your Linux system using popular package managers, such as apt (default on Ubuntu) or flatpak. Then you can launch them as well from the command line and the app will start and be visible inside your Windows OS. But that can cause some friction and is not user-friendly. The really ground-breaking part of this feature is that you can launch them directly from your Windows OS without even starting WSL yourself. Therefore, you can create shortcuts and pin them to the Start menu or taskbar without any friction and really have no need to think about where your app comes from. For the showcase, I have installed Dolphin File Manager and run it through Windows OS. You can see it action below side by side with Windows Explorer.

Image description

Getting started with development on WSL

After hearing all about the cool features of WSL, let’s slowly get back on track with our tutorial. Next up is setting up our dev environment and starting our first app. I’ll be setting up a web dev environment and we’ll use Wasp as an example.

If you aren’t familiar with it, Wasp is a Rails-like framework for React, Node.js, and Prisma. It’s a fast and easy way to develop and deploy your full-stack web apps. For our tutorial, Wasp is a perfect candidate, since it doesn’t support Windows development natively, but only through WSL as it requires a Unix environment.

Let’s get started with installing Node.js first. NVM is the best tool for versioning Node.js, so we want to start with both Node.js and NVM installation.

But first things first, let’s start with Node.js. In WSL, run:

sudo apt install nodejs

in order to install Node on your Linux environment. Next up is NVM. I suggest going to https://github.com/nvm-sh/nvm and getting the latest install script from there. The current download is:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

After this, we have both Node.js and NVM set up in our system.

Installing Wasp

Next up is installing Wasp on our Linux environment. Wasp installation is also pretty straightforward and easy. So just copy and paste this command:

curl -sSL https://get.wasp-lang.dev/installer.sh | sh

and wait for the installer to finish up its thing. Great! But, if you did your WSL setup from 0, you will notice the following warning underneath: It looks like '/home/boris/.local/bin' is not on your PATH! You will not be able to invoke wasp from the terminal by its name.

Image description

Let’s fix this quickly. In order to do this, let’s run

 code ~/.profile

If we don’t already have VS Code, it will automatically set up everything needed and boot up so you can add the command to the end of your file. It will be different for everyone depending on their system name. For example, mine is:

export PATH=$PATH:/home/boris/.local/bin

Setting up VS Code

After setting up Wasp, we want to see how to run the app and access it from VS Code. Under the hood, you will still be using WSL for our development, but we’ll be able to use our VS Code from Host OS (Windows) for most of the things.

Image description

To get started, download the WSL extension to your VS Code in Windows. Afterward, let’s start a new Wasp project to see how it works in action. Open your VS Code Command Palette (ctrl + shift + P) and select the option to “Open Folder in WSL”.

Image description

The folder that I have opened is

\\wsl.localhost\Ubuntu\home\boris\Projects

That is the “Projects” folder inside my home folder in WSL. There are 2 ways for us to know that we are in WSL: The top bar and in the bottom left corner of VS Code. In both places, we have WSL: Ubuntu written, as is shown on screenshots.

Image description

Image description

Once inside this folder, I will open a terminal. It will also be already connected to the proper folder in WSL, so we can get down to business! Let’s run the

wasp new

command to create a new Wasp application. I have chosen the basic template, but you are free to create a project of your choosing, e.g. SaaS starter with GPT, Stripe and more preconfigured. As shown in the screenshot, we should change the current directory of our project to the proper one and then run our project with it.

wasp start

Image description

And just like that, a new screen will open on my Windows machine, showcasing that my Wasp app is open. Cool! My address is still the default localhost:3000, but it is being run from the WSL. Congratulations, you’ve successfully started your first Wasp app through WSL. That wasn’t hard, was it?

Image description

For our final topic, I want to highlight Git workflow with WSL, as it is relatively painless to set up. You can always do the manual git config setup, but I have something cooler for you: Sharing credentials between Windows and WSL. To set up sharing Git credentials, we have to do the following. In Powershell (on Windows), configure the credential manager on Windows.

git config --global credential.helper wincred

And let’s do the same inside WSL.

git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"

This allows us to share our Git username and password. Anything set up in Windows will work in WSL (and vice-versa) and we can use Git inside WSL as we prefer (via VS Code GUI or via shell).

Conclusion

Through our journey here, we have learned what WSL is, how it can be useful for enhancing our workflow with our Windows PC, but also how to set up your initial development environment on it. Microsoft has done a fantastic job with this tool and has really made Windows OS a much more approachable and viable option for all developers. We went through how to install the dev tools needed to kickstart development and how to get a handle on a basic dev workflow. Here are some important links if you want to dive deeper into the topic:

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/12/05/writing-rfcs.html b/blog/2023/12/05/writing-rfcs.html index 69aeebb5f8..a3dc5d69bd 100644 --- a/blog/2023/12/05/writing-rfcs.html +++ b/blog/2023/12/05/writing-rfcs.html @@ -19,13 +19,13 @@ - - + +
-

On the Importance of RFCs in Programming

· 14 min read
Matija Sosic

Imagine you’ve been tasked to implement a sizeable new feature for the product you’re working on. That’s the opportunity you’ve been waiting for - everybody will see what a 10x developer you are! You open a list of the coolest new libraries and design patterns you’ve wanted to try out and get right into it, full “basement” mode. One week later, you victoriously emerge and present your perfect pull request!

But then, the senior dev in a team immediately rejects it - “Too complex, you should have simply used library X and reused Y.”. What!? Before you know it, you’re looking at 100 comments on your PR and days of refactoring to follow.

If only there were a way of knowing about X and Y before implementing everything. Well, it is, and it’s called RFC!

The revelation of RFC

We’ll learn about it through the example of RFC about implementing an authentication system in a web framework Wasp. Wasp is a full-stack web framework built on top of React, Node.js and Prisma. It is used by MAGE, a free GPT-powered codebase generator, which has been used to start over 30,000 applications.

Let's dive in!

So, what is an RFC?

RFC (Request For Comments) is, simply explained, a document proposing a codebase change to solve a specific problem. Its main purpose is to find the best way to solve a problem, as a team effort, before the implementation starts. RFCs were first adopted by the open-source community, but today, they are used in almost any type of developer organization.

RFC overivew
A simplified schema of a typical RFC.

There are other names for this type of document you might encounter in the industry, like TDD (Technical Design Document) or SDD (Software Design Document). Some people argue over the distinction between them, but we won’t.

Fun fact: RFCs were invented by IETF (Internet Engineering Task Force), the engineering organization behind some of the most important internet standards and protocols we use today, like TCP/IP! Not too shabby, right?

When should I write RFC, and when can I skip it?

RFC overivew

So, why bother writing about what you will eventually code, instead of saving time and simply doing it? If you’re dealing with a bug or a relatively simple feature, where it’s very clear what you must do and doesn’t affect project structure, then there’s no need for an RFC - fire up that IDE and get cracking!

But, if you are introducing a completely new concept (e.g., introducing a role-based permission system) or altering the project’s architecture (e.g., adding support for running background jobs), then you might want to take a step back before typing git checkout -b my-new-feature and diving into that sweet coding zone.

All the above being said, sometimes it's not easy to figure out if you should write an RFC or not. Maybe it’s a more prominent feature, but you’ve done something similar before, and you’ve already mapped everything out in your head and pretty much have no questions. To help with that, here’s a simple heuristic I like to use: Is there more than one obvious way to implement this feature? Is there a new library/service we have to pick? If the answer to both of these is “No", you probably don’t need an RFC. Otherwise, there’s a discussion to be had, and RFC is the way to do it.

RFC decision flowchart

It sounds useful. But what’s in it for me?

We’ve established how to decide when to write an RFC, but here is also why you should do it:

  • You will organize your thoughts and get clarity. If you’ve decided to write an RFC, that means you’re dealing with a non-trivial, open-ended problem. Writing things down will help distill your thoughts and have an objective look at them.
  • You will learn more than if you just jumped into coding. You will give yourself space to explore different approaches and oftentimes discover something you haven’t even thought of initially.
  • You will crowdsource your team’s knowledge. By asking your team for feedback (hence Request For Comments), you will get a complete picture of the problem you’re solving and fill in any remaining gaps.
  • You will advance your team’s understanding of the codebase. By collaborating on your RFC, everybody on the team will understand what you’re doing and how you eventually did it. That means next time somebody has to touch that part of the code, they will need to ask you much less questions (=== more uninterrupted coding time!).
  • PR reviews will go much smoother. Remember that situation from the beginning of this article, when your PR got rejected as "too complex"? That’s because the reviewer is missing the context, and you made a sizeable change without a previous buy-in from the rest of the team. By writing an RFC first, you’ll never encounter this type of situation again.
  • Your documentation is already 50% done! To be clear, RFC is not the final documentation, and you cannot simply point to it, but you can likely reuse a lot - images, diagrams, paragraphs, etc.

Wow, this sounds so good that I want to come up with a new feature right now just so I can write an RFC for it! Joke aside, going through with the RFC first makes the coding part so much more enjoyable - you know exactly what you need to do, and you don’t need to question your approach and how it will be received once you create that PR.

Ok, ok, I’m sold! So, how do I go about writing one?

Glad you asked! Many different formats are being used, more or less formal, but I prefer to keep it simple. RFCs that we write at Wasp don’t follow a strict format, but there are some common parts:

  • Metadata - Title, date, reviewers, etc…
  • Problem / Goal
  • Proposed solution (or more of them)
  • Implementation overview
  • Remarks / open questions

That’s pretty much the gist of it! Each of these can be further broken down and refined, but this is the basic outline you can start with.

Let’s now go over each of these and see what they look like in practice, on our Authentication in Wasp example.

Metadata ⌗

RFC metadata

This one is pretty self-explanatory - you will want to track some basic info about your RFCs - status, date of creation, etc.

Some templates also explicitly list the reviewers and the status of their “approval” of the RFC, similar to the PR review process - we don’t have it since we’re a small team where communication happens fast, but it can be handy for larger teams where not everybody knows everybody, and you want to have a bit more of a process in place (e.g. when mentoring junior developers).

RFC reviewer status
Some RFCs require explicit approval by each reviewer.

The problem 🤔

This is where things get interesting. The better you define the problem or the goal/feature you need to implement, and why you need to do it, the easier all the following steps will be. So this is something worth investing in even before you start writing your RFC - make sure you talk to all the involved parties (e.g., product owner, other developers, and even users) to refine your understanding of the issue you’re about to tackle.

By doing this, you will also very likely get first hints and pointers on the possible solutions, and develop a rough sense of the problem space you’re in.

RFC problem definition

Here are a few tips from the example above:

  • Start with a high-level summary - that way, readers can quickly decide if this is relevant to them or not and whether they should keep reading.
  • Provide some context - Explain a bit about the current state of the world, as it is right now. This can be a single sentence or a whole chapter, depending on the intended audience.
  • Clearly state the problem/goal - explain why there is a problem and connect it with the user’s/company’s pain, so that motivation is clear.
  • Provide extra details if possible - diagrams, code examples, … → anything that can help the reader get faster to that “aha” moment. Extra points for using collapsible sections, so the central part of the RFC remains of digestible length.

If you did all this, you’re already well on your way to the excellent RFC! Since defining the problem well is essential, don’t be afraid to add more to it and break things down further.

Non-goals 🛑

This is the sub-section of the "Problem" or "Goal" section that can sometimes be super valuable. Writing what we don't want or will not be doing in this codebase change can help set the expectations and better define its scope.

For example, if we are working on adding a role-based authentication system to our app, people might assume that we will also build some sort of an admin panel for it to manage users and add/remove roles. By explicitly stating it won't be done (and briefly explaining why - not needed, it would take too long, it will be done in the next iteration, ...), reviewers will get a better understanding of what your goal is and you will skip unnecessary discussion.

Solution & Implementation 🛠️

Once we know what we want to do, we have to figure out the best way of doing it! You might have already hinted at the possible solution in the Problem section, but now is the moment to dive deeper - research different approaches, evaluate their pros and cons, and sketch how they could fit into the existing system.

This section is probably the most free-form of all - since it highly depends on the nature of what you are doing, it doesn’t make sense to impose many restrictions here. You may want to stay at the higher level of, e.g., system architecture, or you may need to dive deep into the code and start writing parts of the code you will need. Due to that, I don’t have an exact format for you to follow, but rather a set of guidelines:

Write pseudocode

The purpose of RFC is to convey ideas and principles, not production-grade code that compiles and covers all the edge cases. Feel free to invent/imagine/sketch whatever you need (e.g., imagine you already have a function that sends an email and just use it, even if you don’t), and don’t encumber yourself or the reader with the implementation details (unless that’s exactly what the RFC is about).

It’s better to start at the higher level, and then go deeper when you realize you need it or if one of the reviewers suggests it.

Find out how are others doing it

See what others are doing

How you find this out may differ depending on the type of product you’re developing, but there is almost always a way to do it. If you’re developing an open-source tool like Wasp you can simply check out other popular solutions (that are also open-source) and learn how they did it. If you’re working on a SaaS and need to figure out whether to use cookies or JWTs for the authentication, you likely have some friends who have done it before, and you can ask them. Lastly, simply Google/GPT it.

Why is this so helpful? The reason is that it gives you (and the reviewers) confidence in your solution. If somebody else did it successfully this way, it might be a promising direction. It also might help you discover approaches you haven’t thought of before, or serve as a basis on top of which you can build. Of course, never take anything for granted and take into account the specific needs of your situation, but definitely make use of the knowledge and expertise of others.

Leave things unfinished & don't make it perfect

The main point of RFC is the “C” part, so collaboration (yes, I know it actually stands for "comments"). RFC is not a test where you have to get the perfect score and have no questions asked - if that happens, you probably shouldn’t have written it in the first place.

Solving a problem is a team effort, and you’re just the person taking the first stab at it and pushing things forward. Your task is to lay as much groundwork as you reasonably can (refine the problem, explore multiple approaches to solving it, identify new subproblems that came to light) so the reviewers can quickly grasp the status and provide efficient feedback, directed where it’s needed the most.

The main job of your RFC is to identify the most important problems and direct the reviewer’s attention to them, not solve them.

The RFC you’re writing should be looked at as a discussion area and a work-in-progress, not a piece of art that has to be perfected before it’s displayed in front of the audience.

Remarks & open questions 🎯

In this final section of the document, you can summarise the main thoughts and highlight the biggest open questions. After going through everything, it can be helpful for the reader to be reminded of where his attention can be most valuable.

Now I know when and how to write an RFC! Do you have any templates I could use as a starting point?

Of course! As mentioned, our format is extremely lightweight, but feel free to take a look at the RFC we used as an example to get inspired. Your company could also already have a ready template they recommend.

Here are a few you can use and/or adapt to your needs:

What tool should I use to write my RFCs? There are so many choices!

The exact tool you’re using is probably the least important part of RFC-ing, but it still matters since it sets the workflow around it. If your company has already selected a tool, then of course stick with that. If not, here are the most common choices I’ve come across, along with quick comments:

  • Google Docs - the classic choice. Super easy to comment on any part of the doc, which is the most important feature.
  • Notion - also great for collaboration, plus offers some markdown components such as collapsibles and tables, which can make your RFC more readable.
  • GitHub issues / PRs - this is sometimes used, especially for OSS projects. The drawback is that it is harder to comment on the specific part of the document (you can only comment on the whole line), plus inserting diagrams is also quite clunky. The pro is that everything (code and RFCs) stays on the same platform

We currently use Notion, but any of the above can be a good choice.

Summary

Just as it is the best practice to write a summary at the end of your RFC, we will do the same here! This article came out longer than I expected, but there were so many things to mention - I hope you'll find it useful!

Finally, being able to clearly express your thoughts, formulate the problem, and objectively analyze the possible solutions, with feedback from the team, is what will help you develop the right thing, which is the ultimate productivity hack. This is how you become a 10x engineer.

And don't forget: Weeks of coding can save you hours of planning.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

On the Importance of RFCs in Programming

· 14 min read
Matija Sosic

Imagine you’ve been tasked to implement a sizeable new feature for the product you’re working on. That’s the opportunity you’ve been waiting for - everybody will see what a 10x developer you are! You open a list of the coolest new libraries and design patterns you’ve wanted to try out and get right into it, full “basement” mode. One week later, you victoriously emerge and present your perfect pull request!

But then, the senior dev in a team immediately rejects it - “Too complex, you should have simply used library X and reused Y.”. What!? Before you know it, you’re looking at 100 comments on your PR and days of refactoring to follow.

If only there were a way of knowing about X and Y before implementing everything. Well, it is, and it’s called RFC!

The revelation of RFC

We’ll learn about it through the example of RFC about implementing an authentication system in a web framework Wasp. Wasp is a full-stack web framework built on top of React, Node.js and Prisma. It is used by MAGE, a free GPT-powered codebase generator, which has been used to start over 30,000 applications.

Let's dive in!

So, what is an RFC?

RFC (Request For Comments) is, simply explained, a document proposing a codebase change to solve a specific problem. Its main purpose is to find the best way to solve a problem, as a team effort, before the implementation starts. RFCs were first adopted by the open-source community, but today, they are used in almost any type of developer organization.

RFC overivew
A simplified schema of a typical RFC.

There are other names for this type of document you might encounter in the industry, like TDD (Technical Design Document) or SDD (Software Design Document). Some people argue over the distinction between them, but we won’t.

Fun fact: RFCs were invented by IETF (Internet Engineering Task Force), the engineering organization behind some of the most important internet standards and protocols we use today, like TCP/IP! Not too shabby, right?

When should I write RFC, and when can I skip it?

RFC overivew

So, why bother writing about what you will eventually code, instead of saving time and simply doing it? If you’re dealing with a bug or a relatively simple feature, where it’s very clear what you must do and doesn’t affect project structure, then there’s no need for an RFC - fire up that IDE and get cracking!

But, if you are introducing a completely new concept (e.g., introducing a role-based permission system) or altering the project’s architecture (e.g., adding support for running background jobs), then you might want to take a step back before typing git checkout -b my-new-feature and diving into that sweet coding zone.

All the above being said, sometimes it's not easy to figure out if you should write an RFC or not. Maybe it’s a more prominent feature, but you’ve done something similar before, and you’ve already mapped everything out in your head and pretty much have no questions. To help with that, here’s a simple heuristic I like to use: Is there more than one obvious way to implement this feature? Is there a new library/service we have to pick? If the answer to both of these is “No", you probably don’t need an RFC. Otherwise, there’s a discussion to be had, and RFC is the way to do it.

RFC decision flowchart

It sounds useful. But what’s in it for me?

We’ve established how to decide when to write an RFC, but here is also why you should do it:

  • You will organize your thoughts and get clarity. If you’ve decided to write an RFC, that means you’re dealing with a non-trivial, open-ended problem. Writing things down will help distill your thoughts and have an objective look at them.
  • You will learn more than if you just jumped into coding. You will give yourself space to explore different approaches and oftentimes discover something you haven’t even thought of initially.
  • You will crowdsource your team’s knowledge. By asking your team for feedback (hence Request For Comments), you will get a complete picture of the problem you’re solving and fill in any remaining gaps.
  • You will advance your team’s understanding of the codebase. By collaborating on your RFC, everybody on the team will understand what you’re doing and how you eventually did it. That means next time somebody has to touch that part of the code, they will need to ask you much less questions (=== more uninterrupted coding time!).
  • PR reviews will go much smoother. Remember that situation from the beginning of this article, when your PR got rejected as "too complex"? That’s because the reviewer is missing the context, and you made a sizeable change without a previous buy-in from the rest of the team. By writing an RFC first, you’ll never encounter this type of situation again.
  • Your documentation is already 50% done! To be clear, RFC is not the final documentation, and you cannot simply point to it, but you can likely reuse a lot - images, diagrams, paragraphs, etc.

Wow, this sounds so good that I want to come up with a new feature right now just so I can write an RFC for it! Joke aside, going through with the RFC first makes the coding part so much more enjoyable - you know exactly what you need to do, and you don’t need to question your approach and how it will be received once you create that PR.

Ok, ok, I’m sold! So, how do I go about writing one?

Glad you asked! Many different formats are being used, more or less formal, but I prefer to keep it simple. RFCs that we write at Wasp don’t follow a strict format, but there are some common parts:

  • Metadata - Title, date, reviewers, etc…
  • Problem / Goal
  • Proposed solution (or more of them)
  • Implementation overview
  • Remarks / open questions

That’s pretty much the gist of it! Each of these can be further broken down and refined, but this is the basic outline you can start with.

Let’s now go over each of these and see what they look like in practice, on our Authentication in Wasp example.

Metadata ⌗

RFC metadata

This one is pretty self-explanatory - you will want to track some basic info about your RFCs - status, date of creation, etc.

Some templates also explicitly list the reviewers and the status of their “approval” of the RFC, similar to the PR review process - we don’t have it since we’re a small team where communication happens fast, but it can be handy for larger teams where not everybody knows everybody, and you want to have a bit more of a process in place (e.g. when mentoring junior developers).

RFC reviewer status
Some RFCs require explicit approval by each reviewer.

The problem 🤔

This is where things get interesting. The better you define the problem or the goal/feature you need to implement, and why you need to do it, the easier all the following steps will be. So this is something worth investing in even before you start writing your RFC - make sure you talk to all the involved parties (e.g., product owner, other developers, and even users) to refine your understanding of the issue you’re about to tackle.

By doing this, you will also very likely get first hints and pointers on the possible solutions, and develop a rough sense of the problem space you’re in.

RFC problem definition

Here are a few tips from the example above:

  • Start with a high-level summary - that way, readers can quickly decide if this is relevant to them or not and whether they should keep reading.
  • Provide some context - Explain a bit about the current state of the world, as it is right now. This can be a single sentence or a whole chapter, depending on the intended audience.
  • Clearly state the problem/goal - explain why there is a problem and connect it with the user’s/company’s pain, so that motivation is clear.
  • Provide extra details if possible - diagrams, code examples, … → anything that can help the reader get faster to that “aha” moment. Extra points for using collapsible sections, so the central part of the RFC remains of digestible length.

If you did all this, you’re already well on your way to the excellent RFC! Since defining the problem well is essential, don’t be afraid to add more to it and break things down further.

Non-goals 🛑

This is the sub-section of the "Problem" or "Goal" section that can sometimes be super valuable. Writing what we don't want or will not be doing in this codebase change can help set the expectations and better define its scope.

For example, if we are working on adding a role-based authentication system to our app, people might assume that we will also build some sort of an admin panel for it to manage users and add/remove roles. By explicitly stating it won't be done (and briefly explaining why - not needed, it would take too long, it will be done in the next iteration, ...), reviewers will get a better understanding of what your goal is and you will skip unnecessary discussion.

Solution & Implementation 🛠️

Once we know what we want to do, we have to figure out the best way of doing it! You might have already hinted at the possible solution in the Problem section, but now is the moment to dive deeper - research different approaches, evaluate their pros and cons, and sketch how they could fit into the existing system.

This section is probably the most free-form of all - since it highly depends on the nature of what you are doing, it doesn’t make sense to impose many restrictions here. You may want to stay at the higher level of, e.g., system architecture, or you may need to dive deep into the code and start writing parts of the code you will need. Due to that, I don’t have an exact format for you to follow, but rather a set of guidelines:

Write pseudocode

The purpose of RFC is to convey ideas and principles, not production-grade code that compiles and covers all the edge cases. Feel free to invent/imagine/sketch whatever you need (e.g., imagine you already have a function that sends an email and just use it, even if you don’t), and don’t encumber yourself or the reader with the implementation details (unless that’s exactly what the RFC is about).

It’s better to start at the higher level, and then go deeper when you realize you need it or if one of the reviewers suggests it.

Find out how are others doing it

See what others are doing

How you find this out may differ depending on the type of product you’re developing, but there is almost always a way to do it. If you’re developing an open-source tool like Wasp you can simply check out other popular solutions (that are also open-source) and learn how they did it. If you’re working on a SaaS and need to figure out whether to use cookies or JWTs for the authentication, you likely have some friends who have done it before, and you can ask them. Lastly, simply Google/GPT it.

Why is this so helpful? The reason is that it gives you (and the reviewers) confidence in your solution. If somebody else did it successfully this way, it might be a promising direction. It also might help you discover approaches you haven’t thought of before, or serve as a basis on top of which you can build. Of course, never take anything for granted and take into account the specific needs of your situation, but definitely make use of the knowledge and expertise of others.

Leave things unfinished & don't make it perfect

The main point of RFC is the “C” part, so collaboration (yes, I know it actually stands for "comments"). RFC is not a test where you have to get the perfect score and have no questions asked - if that happens, you probably shouldn’t have written it in the first place.

Solving a problem is a team effort, and you’re just the person taking the first stab at it and pushing things forward. Your task is to lay as much groundwork as you reasonably can (refine the problem, explore multiple approaches to solving it, identify new subproblems that came to light) so the reviewers can quickly grasp the status and provide efficient feedback, directed where it’s needed the most.

The main job of your RFC is to identify the most important problems and direct the reviewer’s attention to them, not solve them.

The RFC you’re writing should be looked at as a discussion area and a work-in-progress, not a piece of art that has to be perfected before it’s displayed in front of the audience.

Remarks & open questions 🎯

In this final section of the document, you can summarise the main thoughts and highlight the biggest open questions. After going through everything, it can be helpful for the reader to be reminded of where his attention can be most valuable.

Now I know when and how to write an RFC! Do you have any templates I could use as a starting point?

Of course! As mentioned, our format is extremely lightweight, but feel free to take a look at the RFC we used as an example to get inspired. Your company could also already have a ready template they recommend.

Here are a few you can use and/or adapt to your needs:

What tool should I use to write my RFCs? There are so many choices!

The exact tool you’re using is probably the least important part of RFC-ing, but it still matters since it sets the workflow around it. If your company has already selected a tool, then of course stick with that. If not, here are the most common choices I’ve come across, along with quick comments:

  • Google Docs - the classic choice. Super easy to comment on any part of the doc, which is the most important feature.
  • Notion - also great for collaboration, plus offers some markdown components such as collapsibles and tables, which can make your RFC more readable.
  • GitHub issues / PRs - this is sometimes used, especially for OSS projects. The drawback is that it is harder to comment on the specific part of the document (you can only comment on the whole line), plus inserting diagrams is also quite clunky. The pro is that everything (code and RFCs) stays on the same platform

We currently use Notion, but any of the above can be a good choice.

Summary

Just as it is the best practice to write a summary at the end of your RFC, we will do the same here! This article came out longer than I expected, but there were so many things to mention - I hope you'll find it useful!

Finally, being able to clearly express your thoughts, formulate the problem, and objectively analyze the possible solutions, with feedback from the team, is what will help you develop the right thing, which is the ultimate productivity hack. This is how you become a 10x engineer.

And don't forget: Weeks of coding can save you hours of planning.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/01/23/wasp-launch-week-five.html b/blog/2024/01/23/wasp-launch-week-five.html index e0d02a461f..a3cc1d4efe 100644 --- a/blog/2024/01/23/wasp-launch-week-five.html +++ b/blog/2024/01/23/wasp-launch-week-five.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #5: Waspnado 🐝 🌪️

· 5 min read
Matija Sosic

Launch Week 5 is here

New Year, New Wasp! That's we at first wanted to use as motto for this launch, but then I bought a DALL-E subscription and typed in "Waspnado, but with plushies". The rest is history.

TL;DR - Wasp is getting dangerously close to 10,000 stars on GitHub and our Discord community is shy of 2,000 members! We're seeing more and more users building and deploying cool apps, both AI-powered, but also good old SaaS-es. Another thing we still have to get used to is getting nice messages from you, such as this one:

Nice testimonial

Thank you! This means a lot to us and reassures our that we are on the right path. Now, without the further ado, let's dive in and see what awaits us this week (a tornado of new features, of course):

Day 1: Wasp Auth 2.0

Auth is one Wasp's flagship, and most popular features. All you need to do is add providers you want to use in Wasp config file (e.g. email, Google, or GitHub) and poof - Wasp will magically create a full-stack auth for you, from the database models to the UI components that you can simply import and use. Here's an 1-minute tour:

And the best part - this all works without any 3rd party services! This is all your code that runs on your infrastructure, and you don't have to pay for it, no matter how many users you have. Pretty neat, huh?

This is all old news, so what's new? I won't spoil too much, but we might have made it even easier to use (no more manually defining data models), plus it now might use a popular library that starts with "L" (and ends with "ucia") under the hood. I won't say anything more!

Read more about it:

Day 2: Wasp, restructured - package.json is back! 🏗️

Pinocchio
Yes, Wasp, you are a real framework now!

This is a big one, and the most complex feature we'll be shipping this week! We've been designing Wasp from day 1 to work nicely with other pieces of the stack, such as React and Node.js. But, some decisions we made in the process on how all these work together weren't the most elegant, both for you as users and us as developers of the framework.

With these changes, Wasp will feel much more like a "real" framework that you are used to - you will be able to use npm install, access your package.json, tsconfig, and more!

Day 3: Wasp AI aka MAGE now lives in your CLI! 🤖 📟

MAGE is an AI-powered, full-stack, React and Node.js web app generator powered by Wasp. All it takes is writing a short description and that's it - you will get a complete codebase you can download, run locally, adjust to your wishes and deploy!

It is one of our most successful products that has been used to kickstart over 30,000 applications!

MAGE in action - browser

So far, you could access MAGE only through it's web interface, hosted at https://usemage.ai/. This is super handy to get started quickly, but what if you already have Wasp installed on your computer, or you want more control over the generation (e.g. use GPT-4 exclusively)? That's when running MAGE from you CLI comes into play! Here's how it works:

This is also the foundation for adding more advanced features to MAGE in the future, like interactive debugging.

Day 4: Open SaaS - freedom to the boilerplate!

Open SaaS revolution

Remember seeing the boilerplate starters costing more than $300, just to start your side project, and then you still have to maintain all that code? Well, we do, and we say no more!

All the best things in the world are free, and there aren't much better things than a feature-rich, production-grade boilerplate starter for React & Node.js with admin dashboard, Stripe and OpenAI integration and more - 100% free and open source! (love is a close second)

You can check it out at https://opensaas.sh/ and give it a star on https://github.com/wasp-lang/open-saas - more details coming soon!

Day 5 - New Year, New Wasp!

Say my name

On the last day of the Launch Week, we're not presenting another feature, but rather a new brand for Wasp (don't worry, Da Boi stays). It will be shorter, sleeker and even easier to remember. Stay tuned and see what this is all about!

Stay in the loop

dont leave

Every day, we'll update this page with the latest announcement of the day - to stay in the loop, follow us on Twitter/X and join our Discord - see you there!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #5: Waspnado 🐝 🌪️

· 5 min read
Matija Sosic

Launch Week 5 is here

New Year, New Wasp! That's we at first wanted to use as motto for this launch, but then I bought a DALL-E subscription and typed in "Waspnado, but with plushies". The rest is history.

TL;DR - Wasp is getting dangerously close to 10,000 stars on GitHub and our Discord community is shy of 2,000 members! We're seeing more and more users building and deploying cool apps, both AI-powered, but also good old SaaS-es. Another thing we still have to get used to is getting nice messages from you, such as this one:

Nice testimonial

Thank you! This means a lot to us and reassures our that we are on the right path. Now, without the further ado, let's dive in and see what awaits us this week (a tornado of new features, of course):

Day 1: Wasp Auth 2.0

Auth is one Wasp's flagship, and most popular features. All you need to do is add providers you want to use in Wasp config file (e.g. email, Google, or GitHub) and poof - Wasp will magically create a full-stack auth for you, from the database models to the UI components that you can simply import and use. Here's an 1-minute tour:

And the best part - this all works without any 3rd party services! This is all your code that runs on your infrastructure, and you don't have to pay for it, no matter how many users you have. Pretty neat, huh?

This is all old news, so what's new? I won't spoil too much, but we might have made it even easier to use (no more manually defining data models), plus it now might use a popular library that starts with "L" (and ends with "ucia") under the hood. I won't say anything more!

Read more about it:

Day 2: Wasp, restructured - package.json is back! 🏗️

Pinocchio
Yes, Wasp, you are a real framework now!

This is a big one, and the most complex feature we'll be shipping this week! We've been designing Wasp from day 1 to work nicely with other pieces of the stack, such as React and Node.js. But, some decisions we made in the process on how all these work together weren't the most elegant, both for you as users and us as developers of the framework.

With these changes, Wasp will feel much more like a "real" framework that you are used to - you will be able to use npm install, access your package.json, tsconfig, and more!

Day 3: Wasp AI aka MAGE now lives in your CLI! 🤖 📟

MAGE is an AI-powered, full-stack, React and Node.js web app generator powered by Wasp. All it takes is writing a short description and that's it - you will get a complete codebase you can download, run locally, adjust to your wishes and deploy!

It is one of our most successful products that has been used to kickstart over 30,000 applications!

MAGE in action - browser

So far, you could access MAGE only through it's web interface, hosted at https://usemage.ai/. This is super handy to get started quickly, but what if you already have Wasp installed on your computer, or you want more control over the generation (e.g. use GPT-4 exclusively)? That's when running MAGE from you CLI comes into play! Here's how it works:

This is also the foundation for adding more advanced features to MAGE in the future, like interactive debugging.

Day 4: Open SaaS - freedom to the boilerplate!

Open SaaS revolution

Remember seeing the boilerplate starters costing more than $300, just to start your side project, and then you still have to maintain all that code? Well, we do, and we say no more!

All the best things in the world are free, and there aren't much better things than a feature-rich, production-grade boilerplate starter for React & Node.js with admin dashboard, Stripe and OpenAI integration and more - 100% free and open source! (love is a close second)

You can check it out at https://opensaas.sh/ and give it a star on https://github.com/wasp-lang/open-saas - more details coming soon!

Day 5 - New Year, New Wasp!

Say my name

On the last day of the Launch Week, we're not presenting another feature, but rather a new brand for Wasp (don't worry, Da Boi stays). It will be shorter, sleeker and even easier to remember. Stay tuned and see what this is all about!

Stay in the loop

dont leave

Every day, we'll update this page with the latest announcement of the day - to stay in the loop, follow us on Twitter/X and join our Discord - see you there!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejs.html b/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejs.html index 53aafdf218..f6a3db6fae 100644 --- a/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejs.html +++ b/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejs.html @@ -19,16 +19,16 @@ - - + +
-

Open SaaS: our free, open-source SaaS starter

· 10 min read
Vinny

Presenting Open SaaS 🎉

We’re really excited to present Open SaaS, the totally free, open-source, production-grade SaaS boilerplate for React, NodeJS, and Prisma.

Check out the promo video here:

Open SaaS has got all the features of those paid SaaS starters you’ve been seeing lately, except its entirely free and open-source.

We felt that paying $300-$2,000 for some boilerplate code that you need to manage yourself was crazy. On top of that, many of these boilerplates rely heavily on 3rd-party services. Add on hosting and other fees, and you’re looking at spending quite a bit of money just to get your idea out there into the world.

That’s why with Open SaaS we made a conscious decision to try and use open-source and free services whenever possible. For example, our hosted demo app and its admin dashboard on OpenSaaS.sh are powered by a self-hosted version of Plausible analytics. Want the same features in your SaaS? Well, Open SaaS has got it preconfigured for you!

Also, the Wasp framework, which Open SaaS uses, does the job of building out a number of features for you, like Auth and Cron Jobs, so that you don’t have to pay a 3rd-party service or code it entirely yourself (we’ll explain this in more detail later).

Before we start...

Open SaaS - Open-source & 100% free React & Node.js SaaS starter! | Product Hunt

Open SaaS is live on Product Hunt right now! Come support our free, open-source initiative 🙏

Image description

Why we built it… and then gave it away for free

The initial feedback in our pre-release has been largely positive, but we’ve also gotten some questions like:

  • “Is it going to stay free?”
  • “What’s your motivation for open-sourcing this?”

So we thought we’d go ahead and answer these to start.

Image description

First, yes it is 100% free and open-source and will stay that way.

Second, we believe that the collective knowledge of a community of developers, indiehackers, and solopreneurs will produce a better boilerplate than an individual or small group. When you buy a SaaS starter from some developer, you’re already getting an opinionated stack, then on top of that you’re also getting an app built the way they think is best — and that may not always be the best for you.

Third, Open SaaS is a project by Wasp, an open-source React + NodeJS + Prisma full-stack framework with superpowers. We, the Wasp team, believe that Wasp is very well suited for creating SaaS apps quickly and efficiently, and we want this template to prove it. Plus, as developers, we’ve learned so much from other open-source projects, and Wasp itself is an open-source project.

Basically, we love the open-source philosophy and we want to pay it forward. 🙏

So it’s our hope that we can provide a seriously valuable asset to the developer community while spreading the word about our open-source, full-stack framework. And we’d love to see the community contribute to it so that it will grow and become the best SaaS boilerplate out there.

What Open SaaS is Made Of

We put a lot of hard work into Open SaaS, including the documentation, so that developers can get a SaaS app launched confidently and easily.

We’ve also spent some time checking out other free, open-source SaaS starters, and wanted to make sure Open SaaS has all the right features of a production-ready starter, without the bloat. And we think we’ve accomplished that for the most part, although we will continue to add features and improve on it with time.

Here are the main features at the moment:

  • 🔐 Authentication (email verified, google, github)
  • 📩 Emailing (sendgrid, emailgun, SMTP)
  • 📈 Admin Dashboard (plausible or google analytics)
  • 🤑 Stripe payments (just add your subscription product IDs)
  • ⌨️ End-to-end Typesafety (no configuration necessary)
  • 🤖 OpenAI integrated (AI-powered example apps)
  • 📖 Blog w/ Astro
  • 🚀 Deploy anywhere
  • 📄 Full Documentation & Community Support

It’s worth going into some detail about each of these features, so let’s do it.

Auth

Image description

Thanks to Wasp, Open SaaS ships with a number of possible Auth methods:

  • username and password (simplest/easiest for dev testing)
  • email verified w/ password reset
  • Google and/or Github social login

Here’s where Wasp really shines, because all it takes to set up your full-stack Auth and get pre-configured UI components is this:

//main.wasp
app SaaSTemplate {
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
google: {},
gitHub: {},
}
}
}

Seriously. That’s it!

Just make sure you’ve set up your social auth and have your API keys, as well as your User and ExternalAuth entities defined, and you’re good to go. And don’t worry, that part is all documented and explained in detail in the Open SaaS Docs.

On top of that, Open SaaS comes preconfigured with some examples on how to customize and create some really powerful auth flows.

Admin Dashboard & Analytics

Image description

By leveraging Wasp’s Jobs feature, Open SaaS pulls data from Plausible’s or Google’s Site Analytics (your choice!) and Stripe’s Data APIs every hour and saves them to our database. This data is then shown on our Admin Dashboard (go to OpenSaaS.sh to see it in action). The nice part is, to get access to this data for your own app, all you have to do is follow our guide on getting your analytics API keys, insert the provided script, and you’re good to go!

Again, Wasp makes this whole process really easy. With the function for querying the APIs and getting the data we need already defined for you, Open SaaS then uses a Wasp Job within the main.wasp config file:

job dailyStatsJob {
executor: PgBoss,
perform: {
fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
},
schedule: {
cron: "0 * * * *"
},
entities: [User, DailyStats, Logs, PageViewSource]
}

And that’s it! Wasp takes care of setting up and running the cron job for you.

Stripe Payments

Image description

If you’re a developer that’s never built your own SaaS before, then integrating with a payments processor like Stripe is probably one of the few challenges you’ll face.

This was the case for me when I built my first SaaS, CoverLetterGPT.xyz. That was actually one of my main motivators for building it; to learn how to intergrate Stripe payments into an app, as well as the OpenAI API.

And even though Stripe is well known for having great documentation, the process can still be daunting. You have to:

  • create the correct product type
  • set up webhook endpoints
  • tell Stripe to send the correct webhook events to you
  • consume the events correctly
  • deal with recurring and failed payments
  • test it all correctly via the CLI before going live

That’s why having Stripe subscription payments set up for you is such a win.

But even more important than that, is having the whole process conveniently documented for you! Which is why Open SaaS offers you convenient Stripe guides in our documentation 🙂

Image description

End-to-End Typesafety

Open SaaS was built with Typescript, and because it’s a full-stack app, type safety from the back-end to the front-end can be a real lifesaver. I mean, some opinionated stacks have gotten hugely popular on this basis.

Luckily, Wasp gives you end-to-end Typesafety out-of-the-box (nothing to configure!), so it was easy for Open SaaS to take advantage of it.

Here’s an example:

  1. Make Wasp aware of your server action:

    // main.wasp

    action getResponse {
    fn: import { getResponse } from "@server/actions.js",
    entities: [Response]
    }
  2. Type and Implement your server action.

    // src/srever/actions.ts

    type RespArgs = {
    hours: string;
    };

    const getResponse: GetResponse<RespArgs, string> = async ({ hours }) => { }
  3. Import it and call it on the client. +

    Open SaaS: our free, open-source SaaS starter

    · 10 min read
    Vinny

    Presenting Open SaaS 🎉

    We’re really excited to present Open SaaS, the totally free, open-source, production-grade SaaS boilerplate for React, NodeJS, and Prisma.

    Check out the promo video here:

    Open SaaS has got all the features of those paid SaaS starters you’ve been seeing lately, except its entirely free and open-source.

    We felt that paying $300-$2,000 for some boilerplate code that you need to manage yourself was crazy. On top of that, many of these boilerplates rely heavily on 3rd-party services. Add on hosting and other fees, and you’re looking at spending quite a bit of money just to get your idea out there into the world.

    That’s why with Open SaaS we made a conscious decision to try and use open-source and free services whenever possible. For example, our hosted demo app and its admin dashboard on OpenSaaS.sh are powered by a self-hosted version of Plausible analytics. Want the same features in your SaaS? Well, Open SaaS has got it preconfigured for you!

    Also, the Wasp framework, which Open SaaS uses, does the job of building out a number of features for you, like Auth and Cron Jobs, so that you don’t have to pay a 3rd-party service or code it entirely yourself (we’ll explain this in more detail later).

    Before we start...

    Open SaaS - Open-source & 100% free React & Node.js SaaS starter! | Product Hunt

    Open SaaS is live on Product Hunt right now! Come support our free, open-source initiative 🙏

    Image description

    Why we built it… and then gave it away for free

    The initial feedback in our pre-release has been largely positive, but we’ve also gotten some questions like:

    • “Is it going to stay free?”
    • “What’s your motivation for open-sourcing this?”

    So we thought we’d go ahead and answer these to start.

    Image description

    First, yes it is 100% free and open-source and will stay that way.

    Second, we believe that the collective knowledge of a community of developers, indiehackers, and solopreneurs will produce a better boilerplate than an individual or small group. When you buy a SaaS starter from some developer, you’re already getting an opinionated stack, then on top of that you’re also getting an app built the way they think is best — and that may not always be the best for you.

    Third, Open SaaS is a project by Wasp, an open-source React + NodeJS + Prisma full-stack framework with superpowers. We, the Wasp team, believe that Wasp is very well suited for creating SaaS apps quickly and efficiently, and we want this template to prove it. Plus, as developers, we’ve learned so much from other open-source projects, and Wasp itself is an open-source project.

    Basically, we love the open-source philosophy and we want to pay it forward. 🙏

    So it’s our hope that we can provide a seriously valuable asset to the developer community while spreading the word about our open-source, full-stack framework. And we’d love to see the community contribute to it so that it will grow and become the best SaaS boilerplate out there.

    What Open SaaS is Made Of

    We put a lot of hard work into Open SaaS, including the documentation, so that developers can get a SaaS app launched confidently and easily.

    We’ve also spent some time checking out other free, open-source SaaS starters, and wanted to make sure Open SaaS has all the right features of a production-ready starter, without the bloat. And we think we’ve accomplished that for the most part, although we will continue to add features and improve on it with time.

    Here are the main features at the moment:

    • 🔐 Authentication (email verified, google, github)
    • 📩 Emailing (sendgrid, emailgun, SMTP)
    • 📈 Admin Dashboard (plausible or google analytics)
    • 🤑 Stripe payments (just add your subscription product IDs)
    • ⌨️ End-to-end Typesafety (no configuration necessary)
    • 🤖 OpenAI integrated (AI-powered example apps)
    • 📖 Blog w/ Astro
    • 🚀 Deploy anywhere
    • 📄 Full Documentation & Community Support

    It’s worth going into some detail about each of these features, so let’s do it.

    Auth

    Image description

    Thanks to Wasp, Open SaaS ships with a number of possible Auth methods:

    • username and password (simplest/easiest for dev testing)
    • email verified w/ password reset
    • Google and/or Github social login

    Here’s where Wasp really shines, because all it takes to set up your full-stack Auth and get pre-configured UI components is this:

    //main.wasp
    app SaaSTemplate {
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    google: {},
    gitHub: {},
    }
    }
    }

    Seriously. That’s it!

    Just make sure you’ve set up your social auth and have your API keys, as well as your User and ExternalAuth entities defined, and you’re good to go. And don’t worry, that part is all documented and explained in detail in the Open SaaS Docs.

    On top of that, Open SaaS comes preconfigured with some examples on how to customize and create some really powerful auth flows.

    Admin Dashboard & Analytics

    Image description

    By leveraging Wasp’s Jobs feature, Open SaaS pulls data from Plausible’s or Google’s Site Analytics (your choice!) and Stripe’s Data APIs every hour and saves them to our database. This data is then shown on our Admin Dashboard (go to OpenSaaS.sh to see it in action). The nice part is, to get access to this data for your own app, all you have to do is follow our guide on getting your analytics API keys, insert the provided script, and you’re good to go!

    Again, Wasp makes this whole process really easy. With the function for querying the APIs and getting the data we need already defined for you, Open SaaS then uses a Wasp Job within the main.wasp config file:

    job dailyStatsJob {
    executor: PgBoss,
    perform: {
    fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
    },
    schedule: {
    cron: "0 * * * *"
    },
    entities: [User, DailyStats, Logs, PageViewSource]
    }

    And that’s it! Wasp takes care of setting up and running the cron job for you.

    Stripe Payments

    Image description

    If you’re a developer that’s never built your own SaaS before, then integrating with a payments processor like Stripe is probably one of the few challenges you’ll face.

    This was the case for me when I built my first SaaS, CoverLetterGPT.xyz. That was actually one of my main motivators for building it; to learn how to intergrate Stripe payments into an app, as well as the OpenAI API.

    And even though Stripe is well known for having great documentation, the process can still be daunting. You have to:

    • create the correct product type
    • set up webhook endpoints
    • tell Stripe to send the correct webhook events to you
    • consume the events correctly
    • deal with recurring and failed payments
    • test it all correctly via the CLI before going live

    That’s why having Stripe subscription payments set up for you is such a win.

    But even more important than that, is having the whole process conveniently documented for you! Which is why Open SaaS offers you convenient Stripe guides in our documentation 🙂

    Image description

    End-to-End Typesafety

    Open SaaS was built with Typescript, and because it’s a full-stack app, type safety from the back-end to the front-end can be a real lifesaver. I mean, some opinionated stacks have gotten hugely popular on this basis.

    Luckily, Wasp gives you end-to-end Typesafety out-of-the-box (nothing to configure!), so it was easy for Open SaaS to take advantage of it.

    Here’s an example:

    1. Make Wasp aware of your server action:

      // main.wasp

      action getResponse {
      fn: import { getResponse } from "@server/actions.js",
      entities: [Response]
      }
    2. Type and Implement your server action.

      // src/srever/actions.ts

      type RespArgs = {
      hours: string;
      };

      const getResponse: GetResponse<RespArgs, string> = async ({ hours }) => { }
    3. Import it and call it on the client. Image description Client-side types will be inferred correctly! Image description

    AI-powered Example App (w/ OpenAI API)

    Image description

    AI is making new app ideas possible, which is partly why we’re seeing a resurgence in developer interest in creating SaaS apps. As I mentioned above, the first SaaS app I built, CoverLetterGPT, is one of those “GPT Wrappers”, and I’m proud to say it makes a nice passive income of ~$350 MRR (monthly recurring revenue).

    I personally believe we’re in a sweet spot in software development where there exists a lot of potential to develop new, profitable AI-powered apps, especially by "indiehackers" and "solopreneurs".

    This is why Open SaaS features an AI scheduling assistant demo app. You input your tasks for along with their alotted time, and the AI Scheduler creates a detailed plan for your day.

    Image description

    Under the hood, this is using OpenAI’s API to assign each task a priority, and break them up into detailed sub-tasks, including coffee breaks! It’s also leverages OpenAI’s function calling feature to return the response back in a user-defined JSON object, so that the client can consume it correctly every time. Also, we're planning on adding open-source LLMs in the future, so stay tuned!

    The demo AI Scheduler is there to help developers learn how to use the OpenAI API effectively, and to spark some creative SaaS app ideas!

    Deploy Anywhere. Easily.

    A lot of the popular SaaS starters out there use hosting-dependent frameworks, which means you're stuck relying on one provider for deployments. While these can be easy options, it may not always be the best for your app.

    Wasp gives you endless possibilities for deploying your full-stack app:

    • One-command deploy to Fly.io with wasp deploy
    • Use wasp build and deploy the Dockerfiles and client wherever you like!

    The great thing about wasp deploy, is that it automatically generates and deploys your database, server, and client, as well as sets up your environment variables for you.

    Open SaaS also has built in environment variable and constants validators to make sure that you’ve got everything correctly set up for deployment, as well as deployment guides in the docs

    Image description

    In the end, you own your code and are free to deploy it wherever, without vendor lock-in.

    Help us, help you

    Open SaaS - Open-source & 100% free React & Node.js SaaS starter! | Product Hunt

    Wanna support our free, open-source initiative? Then go show us some support on Product Hunt right now! 🙏

    Image description

    Now Go Build your SaaS!

    We hope that Open SaaS empowers more developers to ship their ideas and side-projects. And we also hope to get some feedback and input from developers so we can make this the best SaaS boilerplate starter out there.

    So, please, if you have any comments or catch any bugs, submit an issue here.

    And if you’re finding Open SaaS and/or Wasp useful, the easiest way to support is by throwing us a star:

    Discord

    Join our developer community

    Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

    Join our Discord 👾
    📫

    Subscribe to our newsletter

    Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-code.html b/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-code.html index ee344837ae..9e0c8236bd 100644 --- a/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-code.html +++ b/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-code.html @@ -19,13 +19,13 @@ - - + +
-

The first framework that lets you visualize your React/NodeJS app's code

· 9 min read
Vinny

Wasp Studio screenshot, baby sleep tracker

Visualize the Prize

Imagine you’re working on your full-stack app, and you want to implement a new feature. It’s a complicated one, so you whip out a pen and paper, or head over to tldraw, and start drawing a diagram of what your app currently looks like, from database, to server, and on over to the client.

But how cool would it be if you had a tool that visualized your entire full-stack app for you? And what if that tool had the potential to do greater things, like instantly add useful functionality for you across your entire stack, or be paired with AI and Large Language Models for code generation?

Well, that idea is already a reality, and it’s called wasp studio. Check it out here:

Wasp Studio is the Name

First off, Wasp is a full-stack React, NodeJS, and Prisma framework with superpowers. It just crossed 10,000 stars on GitHub, and it has been used to create over 50,000 projects.

Why is it special? It uses a config file and its own compiler to manage a bunch of features for you, like auth, cron jobs, routes, and email sending, saving you tons of time and letting you focus on the fun stuff.

Wasp how-it-works diagram

This combination of Wasp’s central config file, which acts as a set of instructions for the app, and compiler also allow Wasp to do a bunch of complex and interesting tasks for you via one-line commands, such as:

  • full-stack deployments → wasp deploy
  • starting a development database with Docker → wasp start db
  • scaffold entire example apps, such as a SaaS starter → wasp new
  • giving you a visual schematic of your entire full-stack app → wasp studio

If you wanna try them out yourself all you have to do is:

  1. install Wasp with curl -sSL https://get.wasp-lang.dev/installer.sh | sh
  2. scaffold a new To Do app in TypeScript with wasp new -t todo-ts
  3. then to get the visualizer as in the screenshot below, run wasp studio

Baby Sleep Tracker in Wasp Studio

Let’s break down what we’re seeing here real quick:

  • Our main App component in the middle in blue shows the app’s name, database we’re using, and its Auth method
  • Entities to the left in yellow show us which database models we’ve defined
  • Actions and Queries to the far left in red and green show us our server operations that act on our database entities
  • Routes and Pages on the right show us where our React components live and if they require authorization or not (denoted by 🔒)

And if you’re wondering what this might look like with a more complex app, here’s what it looks like when run against Open SaaS - our free, open-source SaaS boilerplate starter.

Open SaaS in Wasp Studio

What’s great about this is that we have an overview of all our database entities and which server functions (aka “operations”) they depend on. In the top left of the picture above, you’ll even see a cron job, dailyStatsJob, which runs every hour (0 * * * *).

This, for example, makes developing backend logic a breeze, especially if you’re not a seasoned backend developer. Consider that the code that gets you there is as simple as this:

job dailyStatsJob {
executor: PgBoss,
perform: {
fn: import { calculateDailyStats } from "@src/calculateDailyStats"
},
schedule: {
cron: "0 * * * *"
},
entities: [User, DailyStats, Logs, PageViewSource]
}

Yep, that’s all it takes for you to get asynchronous jobs on your server. Now your calculateDailyStats function will run every hour — no third party services needed 🙂

Is this a Party Trick!?

Ok. You might be thinking, the visualizer is cool, but does it actually serve a purpose or is it just a nice “party trick”? And to be honest, for now it is a party trick.

But it’s a party trick with a lot of potential up its sleeve. Let me explain.

You got potential, kid.

Of course, you can use it in its current form to get a better perspective of your app, or maybe plan some new features, but in the future you will be able to use it to do a lot more, such as:

  • add new auth methods with a few clicks
  • quickly scaffold functional client-side components with server operations
  • instantly add new full-stack functionality to your entire app, like Stripe payments
  • collaborate easily with Large Language Models (LLMs) to generate features on-the-fly!

Again, this is all possible because of the central configuration file which acts as a set of “instructions” for your app. With this file Wasp literally knows how your app is built, so it can easily display your app to you in visual form. It also makes it a lot easier to build new parts of your app for you in exciting new ways.

Take a look at another snippet from a Wasp config file below. This is all it takes to get full-stack auth for your web app! That’s because the Wasp compiler is managing that boilerplate code for you.

app todoVisualize {
title: "todo-visualize",

auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
google: {},
},
}
}

entity User {=psl
id Int @id @default(autoincrement())
tasks Task[]
psl=}

A Picture is Worth a Thousand Tokens

Now that we know a bit about how Wasp works, let’s dive deeper into the potential of Wasp and wasp studio in combination with LLMs as a future use case.

Currently one of the biggest constraints to AI-assisted code generation is context. By now, we all know that LLMs like to hallucinate, but they also have a pretty bad “memory”. So, if you were to try and get them to build features for your app, to make sure that the new feature works with it, you have to constantly “remind” them of how the app works, its structure, and dependencies.

But with Wasp’s config file, which is essentially just a higher-level abstraction of a full-stack app and its features, we give the LLM the context it needs to successfully build new features for the app at hand.

PG tweet

And this works really well because we don’t only give the LLM the context it needs, but Wasp’s compiler also takes on the responsibility of writing most of the boilerplate for us to begin with (thanks, pal), giving the LLM the simpler tasks of writing, e.g.:

  • modifications to the Wasp config file
  • functions to be run on the server
  • React components that use Wasp code

In this sense, the LLM has to hold a lot less in context and can be forgiven for its bad memory, because Wasp is the one making sure everything stays nicely glued together!

To further bring the point home, let’s take a look again at that Auth code that we introduced above:

auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
google: {},
},

Consider that this code gives auth across your entire stack. So, not only do you get all the auth logic generated and managed for you on the server, but you even get UI components and auth hooks made available to you on the client!

Wasp Auth UI

On the other hand, without the abstractions that Wasp gives us, we end up relying on the LLM, with it’s bad memory and tendency to hallucinate, to write a bunch of boilerplate for us over and over again like this JWT middleware pictured below:

Auth without Wasp

And LLMs are pretty good at coding boilerplatey, repetitive tasks in isolation. But expecting them to do it as part of a cohesive full-stack app means that we have a ton more surface area for exposure to possible errors.

With Wasp, on the other hand, it’s just a few lines of code. If it’s easy for humans to write, it’s also super easy for an LLM to write.

By the way, not only does this save us a lot of headache, it also can save us a lot of money too, as AI-generated Wasp apps use ~10-40x less tokens (i.e. input and output text) than comparable tools, so they generate code at a fraction of the price.

Helping the Computers Help Us

As technologies continue to improve, programming will become more accessible to users with less expert knowledge because more of that expert knowledge will be embedded in our tools.

But that means we will need abstractions that allow for us, the humans, to work easily with these tools.

Like the LLM example above, we can build tools that get AIs to write all the boilerplate for us over and over again, but the question is, should we be letting them do that when they could be doing other more useful things? LLMs are great at producing a wealth of new ideas quickly. Why not build tools that let AIs help us in this regard?

That’s exactly what we have planned for the future of wasp studio. A visual interface that allows you to piece together new features of your app, with or without the help of LLMs, and then A/B test those different ideas quickly.

Not only that, but we also then have an abstraction at our disposal that allows for easy collaboration with users who are less technically inclined. With the help of such tools, even your Product Manager could get in on the fun and start building new features for the developers to sign off on.

What’s so powerful about Wasp and its feature set, is that we get code that’s simpler to read, debug, and maintain, for both people and machines. Coupled with a visual interface, we will be able to quickly iterate on new features across the entire stack, using it as a planning and orchestration tool ourselves, or as a way to more easily debug and oversee the work an LLM might be doing for us.

This is a pretty exciting look at the future of web development and with these new tools will come lots of new ways to utilize them.

What are some ways that you think a tool like wasp studio could be used? What other developments in the realm of AI x Human collaboration can you imagine are coming soon?

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

The first framework that lets you visualize your React/NodeJS app's code

· 9 min read
Vinny

Wasp Studio screenshot, baby sleep tracker

Visualize the Prize

Imagine you’re working on your full-stack app, and you want to implement a new feature. It’s a complicated one, so you whip out a pen and paper, or head over to tldraw, and start drawing a diagram of what your app currently looks like, from database, to server, and on over to the client.

But how cool would it be if you had a tool that visualized your entire full-stack app for you? And what if that tool had the potential to do greater things, like instantly add useful functionality for you across your entire stack, or be paired with AI and Large Language Models for code generation?

Well, that idea is already a reality, and it’s called wasp studio. Check it out here:

Wasp Studio is the Name

First off, Wasp is a full-stack React, NodeJS, and Prisma framework with superpowers. It just crossed 10,000 stars on GitHub, and it has been used to create over 50,000 projects.

Why is it special? It uses a config file and its own compiler to manage a bunch of features for you, like auth, cron jobs, routes, and email sending, saving you tons of time and letting you focus on the fun stuff.

Wasp how-it-works diagram

This combination of Wasp’s central config file, which acts as a set of instructions for the app, and compiler also allow Wasp to do a bunch of complex and interesting tasks for you via one-line commands, such as:

  • full-stack deployments → wasp deploy
  • starting a development database with Docker → wasp start db
  • scaffold entire example apps, such as a SaaS starter → wasp new
  • giving you a visual schematic of your entire full-stack app → wasp studio

If you wanna try them out yourself all you have to do is:

  1. install Wasp with curl -sSL https://get.wasp-lang.dev/installer.sh | sh
  2. scaffold a new To Do app in TypeScript with wasp new -t todo-ts
  3. then to get the visualizer as in the screenshot below, run wasp studio

Baby Sleep Tracker in Wasp Studio

Let’s break down what we’re seeing here real quick:

  • Our main App component in the middle in blue shows the app’s name, database we’re using, and its Auth method
  • Entities to the left in yellow show us which database models we’ve defined
  • Actions and Queries to the far left in red and green show us our server operations that act on our database entities
  • Routes and Pages on the right show us where our React components live and if they require authorization or not (denoted by 🔒)

And if you’re wondering what this might look like with a more complex app, here’s what it looks like when run against Open SaaS - our free, open-source SaaS boilerplate starter.

Open SaaS in Wasp Studio

What’s great about this is that we have an overview of all our database entities and which server functions (aka “operations”) they depend on. In the top left of the picture above, you’ll even see a cron job, dailyStatsJob, which runs every hour (0 * * * *).

This, for example, makes developing backend logic a breeze, especially if you’re not a seasoned backend developer. Consider that the code that gets you there is as simple as this:

job dailyStatsJob {
executor: PgBoss,
perform: {
fn: import { calculateDailyStats } from "@src/calculateDailyStats"
},
schedule: {
cron: "0 * * * *"
},
entities: [User, DailyStats, Logs, PageViewSource]
}

Yep, that’s all it takes for you to get asynchronous jobs on your server. Now your calculateDailyStats function will run every hour — no third party services needed 🙂

Is this a Party Trick!?

Ok. You might be thinking, the visualizer is cool, but does it actually serve a purpose or is it just a nice “party trick”? And to be honest, for now it is a party trick.

But it’s a party trick with a lot of potential up its sleeve. Let me explain.

You got potential, kid.

Of course, you can use it in its current form to get a better perspective of your app, or maybe plan some new features, but in the future you will be able to use it to do a lot more, such as:

  • add new auth methods with a few clicks
  • quickly scaffold functional client-side components with server operations
  • instantly add new full-stack functionality to your entire app, like Stripe payments
  • collaborate easily with Large Language Models (LLMs) to generate features on-the-fly!

Again, this is all possible because of the central configuration file which acts as a set of “instructions” for your app. With this file Wasp literally knows how your app is built, so it can easily display your app to you in visual form. It also makes it a lot easier to build new parts of your app for you in exciting new ways.

Take a look at another snippet from a Wasp config file below. This is all it takes to get full-stack auth for your web app! That’s because the Wasp compiler is managing that boilerplate code for you.

app todoVisualize {
title: "todo-visualize",

auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
google: {},
},
}
}

entity User {=psl
id Int @id @default(autoincrement())
tasks Task[]
psl=}

A Picture is Worth a Thousand Tokens

Now that we know a bit about how Wasp works, let’s dive deeper into the potential of Wasp and wasp studio in combination with LLMs as a future use case.

Currently one of the biggest constraints to AI-assisted code generation is context. By now, we all know that LLMs like to hallucinate, but they also have a pretty bad “memory”. So, if you were to try and get them to build features for your app, to make sure that the new feature works with it, you have to constantly “remind” them of how the app works, its structure, and dependencies.

But with Wasp’s config file, which is essentially just a higher-level abstraction of a full-stack app and its features, we give the LLM the context it needs to successfully build new features for the app at hand.

PG tweet

And this works really well because we don’t only give the LLM the context it needs, but Wasp’s compiler also takes on the responsibility of writing most of the boilerplate for us to begin with (thanks, pal), giving the LLM the simpler tasks of writing, e.g.:

  • modifications to the Wasp config file
  • functions to be run on the server
  • React components that use Wasp code

In this sense, the LLM has to hold a lot less in context and can be forgiven for its bad memory, because Wasp is the one making sure everything stays nicely glued together!

To further bring the point home, let’s take a look again at that Auth code that we introduced above:

auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
google: {},
},

Consider that this code gives auth across your entire stack. So, not only do you get all the auth logic generated and managed for you on the server, but you even get UI components and auth hooks made available to you on the client!

Wasp Auth UI

On the other hand, without the abstractions that Wasp gives us, we end up relying on the LLM, with it’s bad memory and tendency to hallucinate, to write a bunch of boilerplate for us over and over again like this JWT middleware pictured below:

Auth without Wasp

And LLMs are pretty good at coding boilerplatey, repetitive tasks in isolation. But expecting them to do it as part of a cohesive full-stack app means that we have a ton more surface area for exposure to possible errors.

With Wasp, on the other hand, it’s just a few lines of code. If it’s easy for humans to write, it’s also super easy for an LLM to write.

By the way, not only does this save us a lot of headache, it also can save us a lot of money too, as AI-generated Wasp apps use ~10-40x less tokens (i.e. input and output text) than comparable tools, so they generate code at a fraction of the price.

Helping the Computers Help Us

As technologies continue to improve, programming will become more accessible to users with less expert knowledge because more of that expert knowledge will be embedded in our tools.

But that means we will need abstractions that allow for us, the humans, to work easily with these tools.

Like the LLM example above, we can build tools that get AIs to write all the boilerplate for us over and over again, but the question is, should we be letting them do that when they could be doing other more useful things? LLMs are great at producing a wealth of new ideas quickly. Why not build tools that let AIs help us in this regard?

That’s exactly what we have planned for the future of wasp studio. A visual interface that allows you to piece together new features of your app, with or without the help of LLMs, and then A/B test those different ideas quickly.

Not only that, but we also then have an abstraction at our disposal that allows for easy collaboration with users who are less technically inclined. With the help of such tools, even your Product Manager could get in on the fun and start building new features for the developers to sign off on.

What’s so powerful about Wasp and its feature set, is that we get code that’s simpler to read, debug, and maintain, for both people and machines. Coupled with a visual interface, we will be able to quickly iterate on new features across the entire stack, using it as a planning and orchestration tool ourselves, or as a way to more easily debug and oversee the work an LLM might be doing for us.

This is a pretty exciting look at the future of web development and with these new tools will come lots of new ways to utilize them.

What are some ways that you think a tool like wasp studio could be used? What other developments in the realm of AI x Human collaboration can you imagine are coming soon?

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/05/22/how-to-get-a-web-dev-job-2024.html b/blog/2024/05/22/how-to-get-a-web-dev-job-2024.html index 640f0123c7..69074f7023 100644 --- a/blog/2024/05/22/how-to-get-a-web-dev-job-2024.html +++ b/blog/2024/05/22/how-to-get-a-web-dev-job-2024.html @@ -19,13 +19,13 @@ - - + +
-

How to get a Web Dev Job in 2024

· 12 min read
Vinny

Hey, I'm Vince...

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az8xf61b2qxx1msfo4t5.png

I’m a self-taught developer that changed careers during the Covid pandemic. I was able to switch from education to web development by learning and building in my free time, participating in hackathons, and creating educational content for devs.

Back when I was finding my first dev job, although I was determined to become a staff engineer, I started out by taking a very low-paying “traineeship” position. Although it wasn’t ideal, it allowed me to learn on-the-job and get my foot in the door.

A year later, and after a lot of hard work, I got offered a much better position and 3x’ed my previous salary! 🤯

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png

Today, I’m currently working as the founding Developer Relations Engineer for Wasp where I build things like OpenSaaS.sh, a free, open-source SaaS starter template for React and NodeJS, along with Stripe, OpenAI, and AWS S3 integration. It’s based on what I learned from building my first profitable SaaS app, CoverLetterGPT.xyz, which currently has over 100 customers and makes ~$500 per month! Nothing crazy, but something I’m still proud of.

And now that I’m currently in a developer-facing role, I often get asked by people in our community for tips on landing jobs in tech. With this in mind, and with these past experiences under my belt, I thought I’d write a comprehensive article that shares what I’ve learned and seen to be the most effective ways to do so.

Enjoy!

Current Job Market for Developers in 2024

First of all, let’s take a quick look at the current job market for software developers.

Image description

If you spend time on Reddit or X.com (aka Twitter), then you’ve probably seen people complaining about how crappy the current job market is for developers.

To try and find some actual statistics to back up these claims, I used Perplexity.ai to help me find some information on the current demand for software developers, and I was surprised at the results.

Apparently, the demand for software developers remains high, in fact the demand is higher than other jobs, on average, and is expected to grow even more in the coming years!

So why does it feel even harder than usual for some developers to land a job at the moment?

Well, that’s because it actually is harder, but only if you’re a less-experienced developer.

On the other hand, If you’re an experienced dev with a strong portfolio of work, there are a lot more open roles out there for you. But if you’re a junior developer just starting out, the competition is fiercer than ever.

And there are few reasons for that:

  1. Complexity of Skills Required: software development is increasingly complex and requires a broad set of skills, making it difficult for many candidates to meet job requirements.
  2. Remote Work Trends: The shift to remote work has disrupted the entry-level developer pipeline, making it harder for companies to find and train new talent.
  3. Economic Factors: The pandemic and subsequent economic shifts have led to fluctuating hiring patterns, with some periods of high layoffs followed by surges in demand.

Basically, even though there is high demand for experienced developers, there is a comparatively low demand for the less experienced ones.

So with this relatively large supply of beginner and mid-level engineers all competing to get the same jobs, how can you gain the skills of an experienced dev and make yourself stand out from the crowd?

Be a problem solver, not just a coder

A career in software development means that change is a constant. You always have to be ready to learn new things and go outside of your comfort zone because,

  1. the job demands it, and
  2. the industry evolves at an extremely fast pace

In such an environment certificates, courses, and degrees (to a certain extent) matter less, because they don’t prove you have the skill needed to adapt to and solve new problems as they arise. Sure, they prove that you have a certain amount of fundamental knowledge, but that’s only a fraction of the necessary skills needed for the job.

You want to be able to show that you can tackle a challenge that you’ve never faced before, by:

  • quickly learning about this new topic,
  • finding a suitable approach to solving it, and
  • executing on that approach quickly in order to realize your goal

Image description

But don’t just take it from me. AJ, aka Techfren on TikTok, talks about how to navigate the current job environment in a post-AI world. He makes a couple good points that are related to this article here. For example:

  1. General coding knowledge is even less relevant because AI possesses a really broad range of coding knowledge. As an engineer, you’re no longer valuable because you know how to code — an AI now knows how to code pretty damn well (and in a lot more programming languages than you). Your value comes in thinking critically, solving problems, and architecting solutions to those problems.
  2. Businesses will start looking more for these generalist problem solvers to build in-house apps (i.e. internal tools) as replacements to paid services in order to save money and meet their specific business demands, since AI allows developers to be way more productive.

So it’s obvious that problem-solving skills are in high demand, and will continue to be even more important in the future. And we can assume that more experienced, in-demand developers possess those skills, so how do we build them ourselves?

Solve your own Problems

Ok. So you consider yourself to be a curious developer, that can adapt and learn new things quickly, and solve problems on the fly.

But how do you prove this to prospective employers?

Easy. Just solve your own problems! In practice — and in the realm of web development — this means “being on the edge of your comfort zone” and building a web app that’s unique to you and your interests.

Image description

Cameron Blackwood, a self-taught engineer and content creator, describes this perfectly in his TikTok video advising new developers on how to improve their skills. He also has a unique perspective because he previously worked as a tech recruiter, and he says:

  • Build a web app that solves a problem you have in your everyday life
  • Try different things than you’re currently learning / doing at your day job.
  • Keep building and trying new things in your free time.

Of course, these apps you make don’t have to be perfect, but the more unique they are, and the more they show a creative and well-realized solution to a problem, the better.

And if you’re having trouble thinking of things to build, sometimes just experimenting with new tools can inspire new ideas. But however you decide to approach it is up to you, the important thing is to start, so get cracking!


By the way, Wasp is a great way to easily build new apps that solve your unique problems. It’s also one of the quickest ways to build bespoke full-stack apps in React & NodeJS without having to write a bunch of boilerplate code for things like auth, routes, end-to-end typesafety, deployments and more.

As an example, check out this video below which shows you how easy it is to implement full-stack authentication across your entire app.


Do the grunt work

ok

As I was writing this article, I was lucky enough to come across this tweet from Jonathan Stern where he talks about advice he found extremely valuable when he started his first dev job.

Before that job, Jonathan wrote an email to Replit's CEO, Amjad Masad and asked for advice when starting his first job as a software developer.

Here's what Amjad said:

Two ways to prove yourself and make yourself indispensable:

  1. be incredibly productive and inventive -- which is really hard to do when you're starting out

  2. do the boring work that no one wants to do

#2 is available for everyone, it just requires effort and discipline but no one does it, so I would suggest doing that. Incidentally, #2 can often lead to #1 in interesting ways.

Now, even though this is advice for developers who already have a job, I think it is advice that a lot of less experienced devs also looking for jobs should hear.

Amjad’s advice in a broader sense is to basically lower your expectations at first and work hard. Doing the boring work that no one wants to do also might mean doing work that you’re not keen on, but it will benefit you in the long run.

This could also mean taking on jobs that aren’t exactly what you wished for earlier on, and doing the grunt work, in order to become that “indispensable” developer that any employer would love to have on their team.

Be a Good Person

This advice is very general, and can apply to just about any job (or anything), but being a good person to work with is probably a lot more valuable and overlooked than most job seekers imagine.

Once you’ve met the job requirements, a lot of what makes you attractive to prospective employers is whether they could imagine working in a team with you or not. And while that may seem simple and straightforward from the outside, it’s actually a lot harder to put into practice.

Image description

Think about it.

You’ll be working on a team with lots of different personalities. Tasks can get complex, deadlines get tight, and the work can get messy. Mistakes will be definitely be made.

Are you the type of person to lose their sense of humor under pressure?

How will you react when someone blames you for a mistake you weren’t directly responsible for?

Do you communicate openly and effectively with your team?

Will you stay humble and conscientious after 1 year of hard work with no raise? Will you stay humble and conscientious after 1 year of hard work, lots of praise, and a sweet raise (this is probably even harder)?

Being a honest, open, and genuine are valuable traits that are hard to come by, and people can often tell in an instant if you’re that type of person or not. And it’s these type of people, when put up against other candidates that also meet the job requirements, that ultimately end up getting the job offer.

More Effort into Less Applications

One of the things that I and a lot of other employers complain about is when job applicants put little to no effort into their applications. The worst offense is when the application is obviously just a copy-and-paste effort.

typing

Employers hate this because it’s an obvious sign of how you will work on the job. If your job application is done lazily, then it’s very likely your work on the job will be performed similarly (or worse!).

That’s why I think it’s best to put more of your effort into fewer job applications.

There is no magic number, but whenever I was applying to jobs there were always 2 or 3 that I was really excited about. So those were the only ones I applied to, and I put a lot of thought and effort into these applications.

Image description

Besides making my own portfolio with descriptions and learning objectives for my projects, I would also create some form of extra content that was related to the job application. In some instances, this was a simple example app, or in others an explainer video or article.

What was important was that these extra pieces of content were attempts at solving the problems or tasks presented in the job description, to show that I can do that type of work well, and that I’m eager and willing to do the grunt work.

My assumption was that most other applicants wouldn’t go to these lengths when applying and therefore my application would stand out from the crowd, and it worked well as I got asked to interview for many of those positions even without a lot of prior experience!

Now Get That Job…

The software developer job market is changing. It makes sense because the role of the software developer is also constantly evolving, and now that we’re entering the era of AI, these roles are evolving at an even faster pace.

This means, as employers adapt, they’ll probably continue to look for the developers that can prove they’re able to keep up with all these developments, and utilize the tools at hand to solve problems faced in the world around us.

So if you’re able to prove this, while being a conscientious and humble worker, than you probably won’t have such a hard time finding that sweet tech job you’ve always wanted. It’s just a matter of putting in the focus and energy on the right things now, which at times may be hard, that will make the process of finding a job later a whole lot easier.

Thanks for reading and happy job hunting.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to get a Web Dev Job in 2024

· 12 min read
Vinny

Hey, I'm Vince...

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az8xf61b2qxx1msfo4t5.png

I’m a self-taught developer that changed careers during the Covid pandemic. I was able to switch from education to web development by learning and building in my free time, participating in hackathons, and creating educational content for devs.

Back when I was finding my first dev job, although I was determined to become a staff engineer, I started out by taking a very low-paying “traineeship” position. Although it wasn’t ideal, it allowed me to learn on-the-job and get my foot in the door.

A year later, and after a lot of hard work, I got offered a much better position and 3x’ed my previous salary! 🤯

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png

Today, I’m currently working as the founding Developer Relations Engineer for Wasp where I build things like OpenSaaS.sh, a free, open-source SaaS starter template for React and NodeJS, along with Stripe, OpenAI, and AWS S3 integration. It’s based on what I learned from building my first profitable SaaS app, CoverLetterGPT.xyz, which currently has over 100 customers and makes ~$500 per month! Nothing crazy, but something I’m still proud of.

And now that I’m currently in a developer-facing role, I often get asked by people in our community for tips on landing jobs in tech. With this in mind, and with these past experiences under my belt, I thought I’d write a comprehensive article that shares what I’ve learned and seen to be the most effective ways to do so.

Enjoy!

Current Job Market for Developers in 2024

First of all, let’s take a quick look at the current job market for software developers.

Image description

If you spend time on Reddit or X.com (aka Twitter), then you’ve probably seen people complaining about how crappy the current job market is for developers.

To try and find some actual statistics to back up these claims, I used Perplexity.ai to help me find some information on the current demand for software developers, and I was surprised at the results.

Apparently, the demand for software developers remains high, in fact the demand is higher than other jobs, on average, and is expected to grow even more in the coming years!

So why does it feel even harder than usual for some developers to land a job at the moment?

Well, that’s because it actually is harder, but only if you’re a less-experienced developer.

On the other hand, If you’re an experienced dev with a strong portfolio of work, there are a lot more open roles out there for you. But if you’re a junior developer just starting out, the competition is fiercer than ever.

And there are few reasons for that:

  1. Complexity of Skills Required: software development is increasingly complex and requires a broad set of skills, making it difficult for many candidates to meet job requirements.
  2. Remote Work Trends: The shift to remote work has disrupted the entry-level developer pipeline, making it harder for companies to find and train new talent.
  3. Economic Factors: The pandemic and subsequent economic shifts have led to fluctuating hiring patterns, with some periods of high layoffs followed by surges in demand.

Basically, even though there is high demand for experienced developers, there is a comparatively low demand for the less experienced ones.

So with this relatively large supply of beginner and mid-level engineers all competing to get the same jobs, how can you gain the skills of an experienced dev and make yourself stand out from the crowd?

Be a problem solver, not just a coder

A career in software development means that change is a constant. You always have to be ready to learn new things and go outside of your comfort zone because,

  1. the job demands it, and
  2. the industry evolves at an extremely fast pace

In such an environment certificates, courses, and degrees (to a certain extent) matter less, because they don’t prove you have the skill needed to adapt to and solve new problems as they arise. Sure, they prove that you have a certain amount of fundamental knowledge, but that’s only a fraction of the necessary skills needed for the job.

You want to be able to show that you can tackle a challenge that you’ve never faced before, by:

  • quickly learning about this new topic,
  • finding a suitable approach to solving it, and
  • executing on that approach quickly in order to realize your goal

Image description

But don’t just take it from me. AJ, aka Techfren on TikTok, talks about how to navigate the current job environment in a post-AI world. He makes a couple good points that are related to this article here. For example:

  1. General coding knowledge is even less relevant because AI possesses a really broad range of coding knowledge. As an engineer, you’re no longer valuable because you know how to code — an AI now knows how to code pretty damn well (and in a lot more programming languages than you). Your value comes in thinking critically, solving problems, and architecting solutions to those problems.
  2. Businesses will start looking more for these generalist problem solvers to build in-house apps (i.e. internal tools) as replacements to paid services in order to save money and meet their specific business demands, since AI allows developers to be way more productive.

So it’s obvious that problem-solving skills are in high demand, and will continue to be even more important in the future. And we can assume that more experienced, in-demand developers possess those skills, so how do we build them ourselves?

Solve your own Problems

Ok. So you consider yourself to be a curious developer, that can adapt and learn new things quickly, and solve problems on the fly.

But how do you prove this to prospective employers?

Easy. Just solve your own problems! In practice — and in the realm of web development — this means “being on the edge of your comfort zone” and building a web app that’s unique to you and your interests.

Image description

Cameron Blackwood, a self-taught engineer and content creator, describes this perfectly in his TikTok video advising new developers on how to improve their skills. He also has a unique perspective because he previously worked as a tech recruiter, and he says:

  • Build a web app that solves a problem you have in your everyday life
  • Try different things than you’re currently learning / doing at your day job.
  • Keep building and trying new things in your free time.

Of course, these apps you make don’t have to be perfect, but the more unique they are, and the more they show a creative and well-realized solution to a problem, the better.

And if you’re having trouble thinking of things to build, sometimes just experimenting with new tools can inspire new ideas. But however you decide to approach it is up to you, the important thing is to start, so get cracking!


By the way, Wasp is a great way to easily build new apps that solve your unique problems. It’s also one of the quickest ways to build bespoke full-stack apps in React & NodeJS without having to write a bunch of boilerplate code for things like auth, routes, end-to-end typesafety, deployments and more.

As an example, check out this video below which shows you how easy it is to implement full-stack authentication across your entire app.


Do the grunt work

ok

As I was writing this article, I was lucky enough to come across this tweet from Jonathan Stern where he talks about advice he found extremely valuable when he started his first dev job.

Before that job, Jonathan wrote an email to Replit's CEO, Amjad Masad and asked for advice when starting his first job as a software developer.

Here's what Amjad said:

Two ways to prove yourself and make yourself indispensable:

  1. be incredibly productive and inventive -- which is really hard to do when you're starting out

  2. do the boring work that no one wants to do

#2 is available for everyone, it just requires effort and discipline but no one does it, so I would suggest doing that. Incidentally, #2 can often lead to #1 in interesting ways.

Now, even though this is advice for developers who already have a job, I think it is advice that a lot of less experienced devs also looking for jobs should hear.

Amjad’s advice in a broader sense is to basically lower your expectations at first and work hard. Doing the boring work that no one wants to do also might mean doing work that you’re not keen on, but it will benefit you in the long run.

This could also mean taking on jobs that aren’t exactly what you wished for earlier on, and doing the grunt work, in order to become that “indispensable” developer that any employer would love to have on their team.

Be a Good Person

This advice is very general, and can apply to just about any job (or anything), but being a good person to work with is probably a lot more valuable and overlooked than most job seekers imagine.

Once you’ve met the job requirements, a lot of what makes you attractive to prospective employers is whether they could imagine working in a team with you or not. And while that may seem simple and straightforward from the outside, it’s actually a lot harder to put into practice.

Image description

Think about it.

You’ll be working on a team with lots of different personalities. Tasks can get complex, deadlines get tight, and the work can get messy. Mistakes will be definitely be made.

Are you the type of person to lose their sense of humor under pressure?

How will you react when someone blames you for a mistake you weren’t directly responsible for?

Do you communicate openly and effectively with your team?

Will you stay humble and conscientious after 1 year of hard work with no raise? Will you stay humble and conscientious after 1 year of hard work, lots of praise, and a sweet raise (this is probably even harder)?

Being a honest, open, and genuine are valuable traits that are hard to come by, and people can often tell in an instant if you’re that type of person or not. And it’s these type of people, when put up against other candidates that also meet the job requirements, that ultimately end up getting the job offer.

More Effort into Less Applications

One of the things that I and a lot of other employers complain about is when job applicants put little to no effort into their applications. The worst offense is when the application is obviously just a copy-and-paste effort.

typing

Employers hate this because it’s an obvious sign of how you will work on the job. If your job application is done lazily, then it’s very likely your work on the job will be performed similarly (or worse!).

That’s why I think it’s best to put more of your effort into fewer job applications.

There is no magic number, but whenever I was applying to jobs there were always 2 or 3 that I was really excited about. So those were the only ones I applied to, and I put a lot of thought and effort into these applications.

Image description

Besides making my own portfolio with descriptions and learning objectives for my projects, I would also create some form of extra content that was related to the job application. In some instances, this was a simple example app, or in others an explainer video or article.

What was important was that these extra pieces of content were attempts at solving the problems or tasks presented in the job description, to show that I can do that type of work well, and that I’m eager and willing to do the grunt work.

My assumption was that most other applicants wouldn’t go to these lengths when applying and therefore my application would stand out from the crowd, and it worked well as I got asked to interview for many of those positions even without a lot of prior experience!

Now Get That Job…

The software developer job market is changing. It makes sense because the role of the software developer is also constantly evolving, and now that we’re entering the era of AI, these roles are evolving at an even faster pace.

This means, as employers adapt, they’ll probably continue to look for the developers that can prove they’re able to keep up with all these developments, and utilize the tools at hand to solve problems faced in the world around us.

So if you’re able to prove this, while being a conscientious and humble worker, than you probably won’t have such a hard time finding that sweet tech job you’ve always wanted. It’s just a matter of putting in the focus and energy on the right things now, which at times may be hard, that will make the process of finding a job later a whole lot easier.

Thanks for reading and happy job hunting.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yet.html b/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yet.html index eb544c37fc..50a2f404a1 100644 --- a/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yet.html +++ b/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yet.html @@ -19,13 +19,13 @@ - - + +
-

Why We Don't Have a Laravel For JavaScript... Yet

· 12 min read
Vinny

JavaScript's Need for a Full-stack Framework

Why Don't We Have A Laravel For JavaScript?”. This is the question Theo poses in his most recent video.

And if you’re not familiar with tools like Laravel and Ruby-on-Rails, they are opinionated full-stack frameworks (for PHP and Ruby) with lots of built-in features that follow established conventions so that developers can write less boilerplate and more business logic, while getting the industry best practices baked into their app.

Image description

He answers this question with the opinion that JavaScript doesn’t need such frameworks because it’s better to select the tools you want and build the solution you need yourself.

This sounds great — and it also happens to be a nice flex if you’re a seasoned dev — but I feel that he doesn’t back up this claim very well, and I’m here to tell you where I think he’s wrong.

In my opinion, the better question to ask is why don’t we have a Laravel for JavaScript yet? The answer being that we’re still working on it.

In his summary of the full-stack frameworks of the JavaScript world that could be comparable to Laravel or Rails, he fails to consider a few important points:

  1. People really want a Laravel / Rails for JavaScript. If they didn’t, there wouldn’t be so many attempts to create one, and he wouldn’t be making a video whose sole purpose is to respond to the pleading cry “WHY DOESN’T JAVASCRIPT HAVE ITS OWN LARAVEL!?
  2. He fails to consider the timing and maturity of the underlying tools within the JS ecosystem. Perhaps it’s not that a Laravel for JavaScript doesn’t need to exist, it’s just that it doesn’t exist yet due to some major differences in the ecosystems themselves, like how old they are and where the innovation is mostly happening.
  3. He also fails to ask for whom these types of solutions are suitable for. Surely, not all devs have the same objectives, so some might opt for the composable approach while others prefer to reach for a framework.

So let’s take a look at how we got to the point we’re at today, and how we might be able to bring a full-stack framework like Laravel or Rails to the world of JavaScript.

Getting Shit Done

In his video, Theo brings up the point that "there's a common saying in the React world now which is that ‘if you're not using a framework you're building one’”. Even though this is meant to be used as a criticism, Theo feels that most JavaScript devs are missing the point and that building your “own framework” is actually an advantage.

Image description

He feels that the modular nature of the JavaScript ecosystem is a huge advantage, but that sounds like a lot of pressure on the average developer to make unnecessary judgement calls and manage lots of boilerplate code.

Sure, you have teams that need to innovate and meet the needs of special use cases. These are the ones that prioritize modularity. They tweak, improve, and squeeze as much out of developer experience (DX) and performance as possible to get their unique job done right.

But on the other hand, there are also numerous teams whose main objective is producing value and innovating on the side of the product they are building, instead of the tools they are using to build it. These devs will favor a framework that allows them to focus solely on the business logic. This gives them a stable way to build stuff with best practices so they can easily advance from one project to another. In this camp are also the lean, mean indiehackers looking for frameworks so they can move fast and get ideas to market!

Image description

It’s a bit like the difference between Mac and Linux. Mac’s unified stack that just works out-of-the box means many professionals prefer it for its productivity, whereas Linux is great if you’re looking for flexibility and have the time and knowledge to tweak it to your desires. Both are valid solutions that can coexist to meet different needs.

This focus on productivity is what made Rails so powerful back in the day, and why Laravel is such a loved framework at the moment. And the many attempts at creating such a framework for JavaScript is proof enough that there is a large subset of JavaScript devs who also want such a solution.

But maybe the reason such a framework doesn’t exist yet doesn’t have to do with whether devs want one or not, but rather the important factors which are needed in order for such a framework to come together haven’t aligned up until this point. For such a framework to be widely adoptable, it first needs underlying technologies that are stable enough to build upon. After that, it needs time and many iteration cycles to reach maturity itself, so that devs can feel comfortable adopting it.

Have these factors aligned in the JavaScript world to give us the type of frameworks that PHP and Ruby already have? Maybe not quite yet, but they do seem to be slowly coming together.

Comparing Ecosystems

One of Theo’s main points is that JavaScript as a language enables a level of modularity and composability that languages like Ruby and PHP don’t, which is why Ruby and PHP ecosystems are well served by full-stack frameworks, but JavaScript doesn’t need one since you can just compose stuff on your own.

While JavaScript is a peculiar language, with its support for both functional and imperative paradigms and dynamic nature, it also comes with a lot of pitfalls (although it has improved quite a bit lately), so you don’t typically hear it get praised in the way Theo does here. In fact, you are probably more likely to hear praise for Ruby and its properties as a modular and flexible language.

So if it isn’t some unique properties of JavaScript as a language that make it the king of web dev, what is it then?

Image description

Well, the answer is pretty simple: JavaScript is the language of the browser.

Way back when most of the web development was happening on the server side, PHP, Java, Ruby and other languages where reigning supreme. During this era, devs would only write small pieces of functionality in JavaScript, because most of the work was being handled server-side.

But as web development evolved and we started building richer applications, with more dynamic, responsive, and real-time features, a lot of code moved away from the server and over towards JavaScript on the client, because it’s (basically) the only language that supports this. So instead of doing your development mostly in PHP or Ruby with a little bit of JavaScript sprinkled in there, you were now splitting your apps between substantial amounts of JavaScript on the client, plus Ruby or PHP on the server.

JavaScript’s final power move came with the arrival of NodeJS and the ability to also write it on the server, which secured its position as the king of web dev languages. Today, devs can (and do) write their entire apps in JavaScript. This means you need to know one language less, while you’re also able to share the code between front-end and back-end. This has opened up a way for better integration between front-end and back-end, which has snowballed into the ecosystem we know today.

So it’s not so much the unique properties of JavaScript as a language that have made it the dominant ecosystem for web development, but more its unique monopoly as the only language that can be used to write client code, plus it can also be used server-side.

Image description

As Theo says, “we’ve got infinitely more people making awesome solutions” in the JavaScript ecosystem. That’s right. It’s exactly those infinite number of developers working in the space creating the flexibility and modular solutions for JavaScript, rather than it being an innate quality of the programming language.

And because the JavaScript ecosystem is still the hottest one around, it has the most devs in total while continuing to attract new ones every day. This means that we get a large, diverse community doing two main things:

  1. Innovating
  2. Building

The innovators (and influencers) tend to be the loudest, and as a result opinion largely skews in their favor. But there is also a lot of building, or “normal” usage, happening! It’s just that the innovators tend to do the talking on behalf of the builders.

So with all that’s going on in the JavaScript ecosystem, is it pointless to try and build a lasting framework for JavaScript developers, as Theo suggests, or are we on the path towards achieving this goal regardless of what the innovators might claim?

Show Me What You’re Working With

Theo also drops the names of a bunch of current JavaScript frameworks that have either failed to take off, or “just can’t seem to get it right” when it comes to being a comprehensive full-stack solution.

Image description

And he does have a point here. So far, solutions like Blitz, Redwood, Adonis, or T3 haven’t managed to secure the popularity in their ecosystem that Rails or Laravel have in theirs.

But these things take time.

Have a look at the graph above. Laravel and Rails have been around for 13-15 years! The JavaScript frameworks being used in comparison are just getting started, with some of them, like Wasp and Redwood, at similar stages in their development as Laravel and Rails were during their initial years.

As you can see, it takes time for good solutions to reach maturity. And even with some of these frameworks starting to stagnate their great initial growth is evidence that demand for these tools definitely exists!

The main overlying issue that tends to plague these tools is that Javascript as an ecosystem is moving quite fast, so for a solution like this to survive long term, it needs to not only be opinionated enough, but also modular enough to keep up with the shifts in the ecosystem.

Image description

One factor that prevents frameworks from reaching this state is being tied too tightly to the wrong technology. This was NextJS for BlitzJS, GraphQL for Redwood, and Blaze for MeteorJS. And another factor is not going big enough with the framework, because it seems too daunting a task within the JavaScript ecosystem, where things move fast and everyone is “terrified of being opinionated” because they might get criticized by the loudest voices in the scene.

In other words, frameworks that avoid going big on their own, and going truly full-stack, like Ruby-on-Rails and Laravel went, miss the opportunity to solve the most common pain-points that continue to plague JavaScript developers.

But, the JavaScript ecosystem is maturing and stabilizing, we are learning from previous attempts, and there will be a full-stack framework bold enough to go all the way in, get enough things right, and persist for long enough to secure its place.

Say Hi to Wasp

In his comparison of JavaScript frameworks on the market today, Theo also fails to mention the full-stack framework for React & NodeJS that we’re currently working on, Wasp.

We’ve been working hard on Wasp to be the truly full-stack framework that meets the demands of web developers and fills that void in the JavaScript ecosystem to become the framework they love to use.

Image description

With Wasp, we decided to go big, opinionated, and truly full-stack. In other words, we’re going all in with this framework.

That means thinking from first principles and designing a novel approach that only Wasp uses, like building our own compiler for our configuration language, and truly going full-stack, while also keeping it modular enough to move together with the ecosystem as it progresses.

This means that we spent more time in the beginning trying different approaches and building the foundation, which finally brought us a significant jump in usage starting in late 2023. Wasp is now growing strong, and at a really fast pace!

It’s really cool for us to see Wasp being used today to ship tons of new apps and businesses, and even being used internally by some big names and organizations (more info on that will be officially released soon)!

Image description

What Wasp does differently than other full-stack frameworks in the JavaScript world is that it separates it’s main layer of abstraction into its own configuration file, main.wasp. This config file gives Wasp the knowledge it needs to take care of a lot of the boilerplatey, infrastructure-focused code, and allows it to have this unique initial compile-time step where it is able to reason about your web app before it generates the code for it in the background (using that knowledge while generating it).

In practice, this means that all you have to do is describe your Wasp app at a high level in Wasp’s config file, and then implement everything else in technologies that you’re familiar with such as React, NodeJS, and Prisma. It also means that Wasp has a high modularity potential, meaning we are building it to also support other frontend frameworks in the future, like Vue, Solid or Svelte, and to even support additional back-end languages, like Python, Go or Rust.

If you’re the kind of developer that wishes a Rails or Laravel for JavaScript existed, then you should give Wasp a try (and then head into our Discord and let us know what you think)!

Where Are We Headed?

We firmly believe that there will be a full-stack framework for JavaScript as there is Laravel for PHP and Ruby-on-Rails for Ruby.

It just seems like, at the moment, that we’re still working towards it. It also seems very likely that we will get there soon, given the popularity of current meta-frameworks and stacks like NextJS and T3.

But this stuff takes time, and patience.

Plus, you have to be bold enough to try something new, knowing you will get criticized for your work by some of the loudest voices in the ecosystem.

That’s what we’re prepared for and why we’re going all in with Wasp.

See you there!

Image description

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Why We Don't Have a Laravel For JavaScript... Yet

· 12 min read
Vinny

JavaScript's Need for a Full-stack Framework

Why Don't We Have A Laravel For JavaScript?”. This is the question Theo poses in his most recent video.

And if you’re not familiar with tools like Laravel and Ruby-on-Rails, they are opinionated full-stack frameworks (for PHP and Ruby) with lots of built-in features that follow established conventions so that developers can write less boilerplate and more business logic, while getting the industry best practices baked into their app.

Image description

He answers this question with the opinion that JavaScript doesn’t need such frameworks because it’s better to select the tools you want and build the solution you need yourself.

This sounds great — and it also happens to be a nice flex if you’re a seasoned dev — but I feel that he doesn’t back up this claim very well, and I’m here to tell you where I think he’s wrong.

In my opinion, the better question to ask is why don’t we have a Laravel for JavaScript yet? The answer being that we’re still working on it.

In his summary of the full-stack frameworks of the JavaScript world that could be comparable to Laravel or Rails, he fails to consider a few important points:

  1. People really want a Laravel / Rails for JavaScript. If they didn’t, there wouldn’t be so many attempts to create one, and he wouldn’t be making a video whose sole purpose is to respond to the pleading cry “WHY DOESN’T JAVASCRIPT HAVE ITS OWN LARAVEL!?
  2. He fails to consider the timing and maturity of the underlying tools within the JS ecosystem. Perhaps it’s not that a Laravel for JavaScript doesn’t need to exist, it’s just that it doesn’t exist yet due to some major differences in the ecosystems themselves, like how old they are and where the innovation is mostly happening.
  3. He also fails to ask for whom these types of solutions are suitable for. Surely, not all devs have the same objectives, so some might opt for the composable approach while others prefer to reach for a framework.

So let’s take a look at how we got to the point we’re at today, and how we might be able to bring a full-stack framework like Laravel or Rails to the world of JavaScript.

Getting Shit Done

In his video, Theo brings up the point that "there's a common saying in the React world now which is that ‘if you're not using a framework you're building one’”. Even though this is meant to be used as a criticism, Theo feels that most JavaScript devs are missing the point and that building your “own framework” is actually an advantage.

Image description

He feels that the modular nature of the JavaScript ecosystem is a huge advantage, but that sounds like a lot of pressure on the average developer to make unnecessary judgement calls and manage lots of boilerplate code.

Sure, you have teams that need to innovate and meet the needs of special use cases. These are the ones that prioritize modularity. They tweak, improve, and squeeze as much out of developer experience (DX) and performance as possible to get their unique job done right.

But on the other hand, there are also numerous teams whose main objective is producing value and innovating on the side of the product they are building, instead of the tools they are using to build it. These devs will favor a framework that allows them to focus solely on the business logic. This gives them a stable way to build stuff with best practices so they can easily advance from one project to another. In this camp are also the lean, mean indiehackers looking for frameworks so they can move fast and get ideas to market!

Image description

It’s a bit like the difference between Mac and Linux. Mac’s unified stack that just works out-of-the box means many professionals prefer it for its productivity, whereas Linux is great if you’re looking for flexibility and have the time and knowledge to tweak it to your desires. Both are valid solutions that can coexist to meet different needs.

This focus on productivity is what made Rails so powerful back in the day, and why Laravel is such a loved framework at the moment. And the many attempts at creating such a framework for JavaScript is proof enough that there is a large subset of JavaScript devs who also want such a solution.

But maybe the reason such a framework doesn’t exist yet doesn’t have to do with whether devs want one or not, but rather the important factors which are needed in order for such a framework to come together haven’t aligned up until this point. For such a framework to be widely adoptable, it first needs underlying technologies that are stable enough to build upon. After that, it needs time and many iteration cycles to reach maturity itself, so that devs can feel comfortable adopting it.

Have these factors aligned in the JavaScript world to give us the type of frameworks that PHP and Ruby already have? Maybe not quite yet, but they do seem to be slowly coming together.

Comparing Ecosystems

One of Theo’s main points is that JavaScript as a language enables a level of modularity and composability that languages like Ruby and PHP don’t, which is why Ruby and PHP ecosystems are well served by full-stack frameworks, but JavaScript doesn’t need one since you can just compose stuff on your own.

While JavaScript is a peculiar language, with its support for both functional and imperative paradigms and dynamic nature, it also comes with a lot of pitfalls (although it has improved quite a bit lately), so you don’t typically hear it get praised in the way Theo does here. In fact, you are probably more likely to hear praise for Ruby and its properties as a modular and flexible language.

So if it isn’t some unique properties of JavaScript as a language that make it the king of web dev, what is it then?

Image description

Well, the answer is pretty simple: JavaScript is the language of the browser.

Way back when most of the web development was happening on the server side, PHP, Java, Ruby and other languages where reigning supreme. During this era, devs would only write small pieces of functionality in JavaScript, because most of the work was being handled server-side.

But as web development evolved and we started building richer applications, with more dynamic, responsive, and real-time features, a lot of code moved away from the server and over towards JavaScript on the client, because it’s (basically) the only language that supports this. So instead of doing your development mostly in PHP or Ruby with a little bit of JavaScript sprinkled in there, you were now splitting your apps between substantial amounts of JavaScript on the client, plus Ruby or PHP on the server.

JavaScript’s final power move came with the arrival of NodeJS and the ability to also write it on the server, which secured its position as the king of web dev languages. Today, devs can (and do) write their entire apps in JavaScript. This means you need to know one language less, while you’re also able to share the code between front-end and back-end. This has opened up a way for better integration between front-end and back-end, which has snowballed into the ecosystem we know today.

So it’s not so much the unique properties of JavaScript as a language that have made it the dominant ecosystem for web development, but more its unique monopoly as the only language that can be used to write client code, plus it can also be used server-side.

Image description

As Theo says, “we’ve got infinitely more people making awesome solutions” in the JavaScript ecosystem. That’s right. It’s exactly those infinite number of developers working in the space creating the flexibility and modular solutions for JavaScript, rather than it being an innate quality of the programming language.

And because the JavaScript ecosystem is still the hottest one around, it has the most devs in total while continuing to attract new ones every day. This means that we get a large, diverse community doing two main things:

  1. Innovating
  2. Building

The innovators (and influencers) tend to be the loudest, and as a result opinion largely skews in their favor. But there is also a lot of building, or “normal” usage, happening! It’s just that the innovators tend to do the talking on behalf of the builders.

So with all that’s going on in the JavaScript ecosystem, is it pointless to try and build a lasting framework for JavaScript developers, as Theo suggests, or are we on the path towards achieving this goal regardless of what the innovators might claim?

Show Me What You’re Working With

Theo also drops the names of a bunch of current JavaScript frameworks that have either failed to take off, or “just can’t seem to get it right” when it comes to being a comprehensive full-stack solution.

Image description

And he does have a point here. So far, solutions like Blitz, Redwood, Adonis, or T3 haven’t managed to secure the popularity in their ecosystem that Rails or Laravel have in theirs.

But these things take time.

Have a look at the graph above. Laravel and Rails have been around for 13-15 years! The JavaScript frameworks being used in comparison are just getting started, with some of them, like Wasp and Redwood, at similar stages in their development as Laravel and Rails were during their initial years.

As you can see, it takes time for good solutions to reach maturity. And even with some of these frameworks starting to stagnate their great initial growth is evidence that demand for these tools definitely exists!

The main overlying issue that tends to plague these tools is that Javascript as an ecosystem is moving quite fast, so for a solution like this to survive long term, it needs to not only be opinionated enough, but also modular enough to keep up with the shifts in the ecosystem.

Image description

One factor that prevents frameworks from reaching this state is being tied too tightly to the wrong technology. This was NextJS for BlitzJS, GraphQL for Redwood, and Blaze for MeteorJS. And another factor is not going big enough with the framework, because it seems too daunting a task within the JavaScript ecosystem, where things move fast and everyone is “terrified of being opinionated” because they might get criticized by the loudest voices in the scene.

In other words, frameworks that avoid going big on their own, and going truly full-stack, like Ruby-on-Rails and Laravel went, miss the opportunity to solve the most common pain-points that continue to plague JavaScript developers.

But, the JavaScript ecosystem is maturing and stabilizing, we are learning from previous attempts, and there will be a full-stack framework bold enough to go all the way in, get enough things right, and persist for long enough to secure its place.

Say Hi to Wasp

In his comparison of JavaScript frameworks on the market today, Theo also fails to mention the full-stack framework for React & NodeJS that we’re currently working on, Wasp.

We’ve been working hard on Wasp to be the truly full-stack framework that meets the demands of web developers and fills that void in the JavaScript ecosystem to become the framework they love to use.

Image description

With Wasp, we decided to go big, opinionated, and truly full-stack. In other words, we’re going all in with this framework.

That means thinking from first principles and designing a novel approach that only Wasp uses, like building our own compiler for our configuration language, and truly going full-stack, while also keeping it modular enough to move together with the ecosystem as it progresses.

This means that we spent more time in the beginning trying different approaches and building the foundation, which finally brought us a significant jump in usage starting in late 2023. Wasp is now growing strong, and at a really fast pace!

It’s really cool for us to see Wasp being used today to ship tons of new apps and businesses, and even being used internally by some big names and organizations (more info on that will be officially released soon)!

Image description

What Wasp does differently than other full-stack frameworks in the JavaScript world is that it separates it’s main layer of abstraction into its own configuration file, main.wasp. This config file gives Wasp the knowledge it needs to take care of a lot of the boilerplatey, infrastructure-focused code, and allows it to have this unique initial compile-time step where it is able to reason about your web app before it generates the code for it in the background (using that knowledge while generating it).

In practice, this means that all you have to do is describe your Wasp app at a high level in Wasp’s config file, and then implement everything else in technologies that you’re familiar with such as React, NodeJS, and Prisma. It also means that Wasp has a high modularity potential, meaning we are building it to also support other frontend frameworks in the future, like Vue, Solid or Svelte, and to even support additional back-end languages, like Python, Go or Rust.

If you’re the kind of developer that wishes a Rails or Laravel for JavaScript existed, then you should give Wasp a try (and then head into our Discord and let us know what you think)!

Where Are We Headed?

We firmly believe that there will be a full-stack framework for JavaScript as there is Laravel for PHP and Ruby-on-Rails for Ruby.

It just seems like, at the moment, that we’re still working towards it. It also seems very likely that we will get there soon, given the popularity of current meta-frameworks and stacks like NextJS and T3.

But this stuff takes time, and patience.

Plus, you have to be bold enough to try something new, knowing you will get criticized for your work by some of the loudest voices in the ecosystem.

That’s what we’re prepared for and why we’re going all in with Wasp.

See you there!

Image description

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/07/03/building-selling-saas-in-5-months.html b/blog/2024/07/03/building-selling-saas-in-5-months.html index 7f311c7741..1b3600def0 100644 --- a/blog/2024/07/03/building-selling-saas-in-5-months.html +++ b/blog/2024/07/03/building-selling-saas-in-5-months.html @@ -19,13 +19,13 @@ - - + +
-

Building and Selling a GPT Wrapper SaaS in 5 Months

· 7 min read
Vinny

Since the release of ChatGPT, we’ve been flooded with all possible versions of apps that use it in one way or another. Building on top of trendy technology is an excellent way to get initial attention, but still, 99% of these apps die very quickly and don’t last beyond a week or two following their “big” Twitter or Product Hunt launch.

Why? Because they aren’t solving a real problem. It’s either a fun tech gadget or a gross overpromise (e.g., “you will never need to code again,” which I strongly disagree with) that quickly falls short.

Building a successful product still follows the same rules as in the pre-GPT era: find a problem people are willing to pay for and then figure out a way to reach these people. Sounds simple? It is, but it for sure isn’t easy. The good news is that GPT opened so many new opportunities that actually doing it is faster and easier than ever.

Meet the hero of our story - Max! 🦸

our-hero-max

The hero of our story today is Max, a software engineer at Red Hat. He built https://description-generator.online (an AI description generator for Etsy products) and sold it on acquire.com. A senior backend engineer by day and a serial hacker and tinkerer by night, Max always had a passion for building products, and GPT was the last piece of the puzzle he was waiting for.

Read on to learn how he went through the entire cycle of finding a problem, building a solution, getting customers, and ultimately selling his app in 5 months total.

Lesson #1: Look for problems in “unusual” places 🕵️‍♂️

Looking for problems

TL;DR: Talk to your friends who aren’t developers! Learn about their problems and offer help. The more unfamiliar and disconnected from tech their occupation is, the better - these are gold mines for GPT-powered solutions!

It all started with Max’s friend who owns an Etsy marketplace - she needed help with some data/workflow automation, and Max agreed to lend a hand. Consequently, he also started hanging out in the Ukranian Etsy community on Slack.

Soon, he learned that one of the most common requests there is for help with writing product descriptions (”listings”) in English. Although most members used English daily and had no problem communicating, writing high-quality, compelling, and professional-sounding listings was still a challenge. Auto-translation services still weren’t sophisticated enough, and hiring native English speakers was too expensive for most.

This sounded like a real, glaring problem directly connected to the number of items Etsy sellers sell and, thus, the profit they make. As it turned out, it was the case.

Lesson #2: Build a prototype, fast 🏎️

Image description

TL;DR: Speed is the name of the game here. Don’t spend time flexing on your stack and optimizing to the last byte. Pick something that works and ship it!

The problem of writing convincing product listings in English caught Max’s attention. He was aware of ChatGPT and how useful it could be for this. However, being a backend engineer with limited frontend experience, building a full-stack app around it and choosing and configuring all parts of the stack himself sounded daunting and laborious. It wasn’t until he came across Open SaaS that he felt ready to take action.

The prototype was ready after a couple of days, and Max immediately shared it with his Etsy community. He kept it extremely simple - no landing page or any copy at all (just a form to enter your product details), even no custom domain yet, but myProduct.fly.io you get assigned upon deploying to Fly (which takes just a single CLI command with Wasp).

And that was enough - as his product scratched the itch Etsy sellers repeatedly mentioned, the reception was overwhelmingly positive! In just a few days, Max got 400 signups, and several hundred product listings were generated daily.


By the way, if you’re looking for an easy, low maintenance way to start your next side project, check out Open SaaS, a 100% free, open-source Saas Starter!

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png

Open SaaS is a feature-rich, React + NodeJS SaaS template, with Stripe, OpenAI / GPT app examples, AWS S3 file upload, Analytics, Admin Dashboard, and full Documentation!


Lesson #3: Test willingness to pay early 💸

money please

TL;DR: People signing up for your product is amazing, but convincing them to pay is a completely separate game. If you want to ensure your solution brings real value and you’re not wasting time, find a way to test monetizing as early as possible.

Max saw the adoption picking up, which made him ask himself “How do I turn this into a business? What would users be willing to pay for?” After all, he had his own expenses, like server costs and GPT API subscription.

Looking at how users use the product, he quickly realized he could make generating descriptions even easier - a seller could upload the image of a product, and that’s it; the full product description can be generated directly from it. That was a good candidate for a “premium” feature, since it was an upgrade on top of the basic functionality.

Max added the feature, and soon enough, the first customers started rolling in! 💰

Lesson #4: Keep building or sell? How to decide 🤔

homer selling

TL;DR: Is the market’s domain something you’re personally excited about and see yourself in for the long term? Do you feel your competitive advantage will grow stronger with time? If yes, keep building. Otherwise, sell!

description-generator.online now had both users and first revenue, amazing! Still, soon, it became apparent that the Etsy community Max was part of had its limits. Although all non-English speaking markets shared the problem, which made for a big opportunity, reaching them and setting up and executing a sales process would still take time and effort.

On the other hand, competing products started appearing. Although super valuable for Etsy sellers, if Max built the product in a week, others could do it too. It started becoming clear that the value of the business would soon start moving from the technical solution to sales, support, and customer experience.

Being a hacker at heart and not so personally invested in arts & crafts marketplaces, Max decided to sell the product to somebody who is. He listed the description generator on https://acquire.com/, along with the usage metrics and relevant data, and soon started receiving offers.

Lesson #5: Provide support during acquisition 🤝

got my back

TL;DR: Selling your product takes more than finding a buyer. Providing impeccable support during acquisition is just as important as building the product.

Finding a buyer and agreeing on a price took about a month. Since the buyer was taking over everything - the source code, domain, and customers, Max providing 3-month support with the transition was an essential part of the deal.

Also, since they couldn’t use an escrow service due to some technical and geographical limitations, they agreed on splitting the payment 50/50 - half in the beginning and another half when the migration was over. Max made sure his customers had a flawless experience with moving everything over, resulting in a great relationship mutually filled with trust. Besides selling your app, making friends is an underrated bonus! 😎

After a few months, the deal has been reached! Description-generator.online got a new owner, an expert in the industry willing to expand to new markets, and Max got his first exit and could move on to the next exciting project!

Summary

michael summary

That’s it! Building a product others find helpful so much they’re willing to pay for it is a deeply gratifying experience. We saw how Max did it and what lessons he learned along the way:

  1. Look for problems in “unusual” places
  2. Build a prototype fast
  3. Test willingness to pay early
  4. Decide whether you want to keep building or sell
  5. Provide support during the acquisition

Hopefully, this was helpful!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Building and Selling a GPT Wrapper SaaS in 5 Months

· 7 min read
Vinny

Since the release of ChatGPT, we’ve been flooded with all possible versions of apps that use it in one way or another. Building on top of trendy technology is an excellent way to get initial attention, but still, 99% of these apps die very quickly and don’t last beyond a week or two following their “big” Twitter or Product Hunt launch.

Why? Because they aren’t solving a real problem. It’s either a fun tech gadget or a gross overpromise (e.g., “you will never need to code again,” which I strongly disagree with) that quickly falls short.

Building a successful product still follows the same rules as in the pre-GPT era: find a problem people are willing to pay for and then figure out a way to reach these people. Sounds simple? It is, but it for sure isn’t easy. The good news is that GPT opened so many new opportunities that actually doing it is faster and easier than ever.

Meet the hero of our story - Max! 🦸

our-hero-max

The hero of our story today is Max, a software engineer at Red Hat. He built https://description-generator.online (an AI description generator for Etsy products) and sold it on acquire.com. A senior backend engineer by day and a serial hacker and tinkerer by night, Max always had a passion for building products, and GPT was the last piece of the puzzle he was waiting for.

Read on to learn how he went through the entire cycle of finding a problem, building a solution, getting customers, and ultimately selling his app in 5 months total.

Lesson #1: Look for problems in “unusual” places 🕵️‍♂️

Looking for problems

TL;DR: Talk to your friends who aren’t developers! Learn about their problems and offer help. The more unfamiliar and disconnected from tech their occupation is, the better - these are gold mines for GPT-powered solutions!

It all started with Max’s friend who owns an Etsy marketplace - she needed help with some data/workflow automation, and Max agreed to lend a hand. Consequently, he also started hanging out in the Ukranian Etsy community on Slack.

Soon, he learned that one of the most common requests there is for help with writing product descriptions (”listings”) in English. Although most members used English daily and had no problem communicating, writing high-quality, compelling, and professional-sounding listings was still a challenge. Auto-translation services still weren’t sophisticated enough, and hiring native English speakers was too expensive for most.

This sounded like a real, glaring problem directly connected to the number of items Etsy sellers sell and, thus, the profit they make. As it turned out, it was the case.

Lesson #2: Build a prototype, fast 🏎️

Image description

TL;DR: Speed is the name of the game here. Don’t spend time flexing on your stack and optimizing to the last byte. Pick something that works and ship it!

The problem of writing convincing product listings in English caught Max’s attention. He was aware of ChatGPT and how useful it could be for this. However, being a backend engineer with limited frontend experience, building a full-stack app around it and choosing and configuring all parts of the stack himself sounded daunting and laborious. It wasn’t until he came across Open SaaS that he felt ready to take action.

The prototype was ready after a couple of days, and Max immediately shared it with his Etsy community. He kept it extremely simple - no landing page or any copy at all (just a form to enter your product details), even no custom domain yet, but myProduct.fly.io you get assigned upon deploying to Fly (which takes just a single CLI command with Wasp).

And that was enough - as his product scratched the itch Etsy sellers repeatedly mentioned, the reception was overwhelmingly positive! In just a few days, Max got 400 signups, and several hundred product listings were generated daily.


By the way, if you’re looking for an easy, low maintenance way to start your next side project, check out Open SaaS, a 100% free, open-source Saas Starter!

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png

Open SaaS is a feature-rich, React + NodeJS SaaS template, with Stripe, OpenAI / GPT app examples, AWS S3 file upload, Analytics, Admin Dashboard, and full Documentation!


Lesson #3: Test willingness to pay early 💸

money please

TL;DR: People signing up for your product is amazing, but convincing them to pay is a completely separate game. If you want to ensure your solution brings real value and you’re not wasting time, find a way to test monetizing as early as possible.

Max saw the adoption picking up, which made him ask himself “How do I turn this into a business? What would users be willing to pay for?” After all, he had his own expenses, like server costs and GPT API subscription.

Looking at how users use the product, he quickly realized he could make generating descriptions even easier - a seller could upload the image of a product, and that’s it; the full product description can be generated directly from it. That was a good candidate for a “premium” feature, since it was an upgrade on top of the basic functionality.

Max added the feature, and soon enough, the first customers started rolling in! 💰

Lesson #4: Keep building or sell? How to decide 🤔

homer selling

TL;DR: Is the market’s domain something you’re personally excited about and see yourself in for the long term? Do you feel your competitive advantage will grow stronger with time? If yes, keep building. Otherwise, sell!

description-generator.online now had both users and first revenue, amazing! Still, soon, it became apparent that the Etsy community Max was part of had its limits. Although all non-English speaking markets shared the problem, which made for a big opportunity, reaching them and setting up and executing a sales process would still take time and effort.

On the other hand, competing products started appearing. Although super valuable for Etsy sellers, if Max built the product in a week, others could do it too. It started becoming clear that the value of the business would soon start moving from the technical solution to sales, support, and customer experience.

Being a hacker at heart and not so personally invested in arts & crafts marketplaces, Max decided to sell the product to somebody who is. He listed the description generator on https://acquire.com/, along with the usage metrics and relevant data, and soon started receiving offers.

Lesson #5: Provide support during acquisition 🤝

got my back

TL;DR: Selling your product takes more than finding a buyer. Providing impeccable support during acquisition is just as important as building the product.

Finding a buyer and agreeing on a price took about a month. Since the buyer was taking over everything - the source code, domain, and customers, Max providing 3-month support with the transition was an essential part of the deal.

Also, since they couldn’t use an escrow service due to some technical and geographical limitations, they agreed on splitting the payment 50/50 - half in the beginning and another half when the migration was over. Max made sure his customers had a flawless experience with moving everything over, resulting in a great relationship mutually filled with trust. Besides selling your app, making friends is an underrated bonus! 😎

After a few months, the deal has been reached! Description-generator.online got a new owner, an expert in the industry willing to expand to new markets, and Max got his first exit and could move on to the next exciting project!

Summary

michael summary

That’s it! Building a product others find helpful so much they’re willing to pay for it is a deeply gratifying experience. We saw how Max did it and what lessons he learned along the way:

  1. Look for problems in “unusual” places
  2. Build a prototype fast
  3. Test willingness to pay early
  4. Decide whether you want to keep building or sell
  5. Provide support during the acquisition

Hopefully, this was helpful!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/07/15/wasp-launch-week-six.html b/blog/2024/07/15/wasp-launch-week-six.html index ad9dbdcdd6..9adb92d735 100644 --- a/blog/2024/07/15/wasp-launch-week-six.html +++ b/blog/2024/07/15/wasp-launch-week-six.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

· 5 min read
Matija Sosic

Launch Week 6 is here

Bonjour Wasp connoisseurs 🐝 👋,

It's been a while, but we're back! We've been busy wasps and put our antennae down to work to deliver you v0.14, which is happening in exactly two days, on July 17th!

It's happening!

To reserve your spot (we can fit only so many people in our Discord), click here.

Join the kick-off event

Once you see the invite, mark yourself as "Interested," and that's it (don't make us talk to ourselves again)! Also, if you thought you'd slip by without a bad joke, you were direly wrong:

Why do wasps never leave tips? Because they are stingy.

🤯🤯🤯

Okay, now to the fun stuff—let's see what we're packing into this upcoming release!

#1: 📝 Define your data models in a separate prisma.schema file!

This change marks the beginning of one of our most requested features - splitting your Wasp config into multiple files! We know that as you develop your Wasp app and it grows, it can become unwieldy to have everything in one hefty .wasp file.

Define data models in prisma.schema file

That's why we started by extracting the data model definitions into a standalone Prisma schema file! This will significantly reduce the size of the Wasp config file and also allow for a more streamlined experience of writing PSL (Prisma Schema Language), with all the goodies like syntax highlighting and auto-completion working out of the box 🎉.

#2: 🔒 Auth Hooks - onBeforeSignup, onAfterSignup, and more!

Auth lifecycle hooks

Although Wasp's Auth feature is probably the fastest way to get authentication running in your full-stack app, adding your custom logic to the auth process can also be quite handy—e.g. if you want to log something, do some extra config etc.

We've made that easy, by offering several authentication lifecycle hooks that you can use for the exact purpose!

#3: 🆕 New authentication provider: Discord!

Discord as a new auth provider

This one is pretty self-explanatory, but that doesn't make it any less cool! Besides Google and GitHub, Discord is now a third social auth method natively supported by Wasp, next to Google and GitHub - that means all you need to do is define a single line in your Wasp config, and voilà - your users can now sign in with Discord!

#4: 👀 TypeScript SDK RFC - a sneak peek!

TS SDK proposal of code

As you might have seen in the community, this has been an ongoing topic of discussion for a while. Although having a dedicated configuration language (DSL) allows for the maximum customizability of the DX, having Wasp config in TypeScript instead will help out with language tooling (IDE syntax highlighting and auto-completion). Also, it might feel even more familiar to developers using Wasp.

This is why we decided to test the waters and see how we (and you) like it! We are still working out what it will look like, and we have laid out some of the ideas in this RFC for TS SDK. We'd love to hear from you and get your comments and ideas in this GitHub issue (or just come to our Discord and bash us there 😅)

#5: 🤯 OpenSaaS, Reloaded!

Open SaaS banner

And finally, the star of the last launch week, Open SaaS, a 100% free and open-source boilerplate starter for React & Node.js, powered by Wasp, has received its first makeover! You gave us a ton of amazing feedback and ideas, and we listened. Here's what's new:

  • Simplified Stripe payment logic
  • The code is now organized vertically by features (instead of client and server folders)
  • Introduced e2e tests with Playwright
  • Added an optional cookie consent banner that hooks up to Google Analytics
  • Bunch of small bug fixes and docs updates

And more! Don't forget that Lambo you will earn with your SaaS is just within arm's reach (well, it depends on how long your arm is). All you have to do is go to OpenSaaS and finally start that app you've been dreaming about 🏎️.

#6: 🫵 See you there!

That's pretty much it—we've given you a taste of what's coming, but for the real deal, you'll have to join us on Wednesday! Register here and make sure to mark yourself as interested—we'll see you there if you don't see us first (sorry)!

Join the kick-off event

Register for the kick-off event here.

Stay in the loop

dont leave

Every day, we'll update this page with the latest announcement of the day - to stay in the loop, follow us on Twitter/X and join our Discord - see you there!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

· 5 min read
Matija Sosic

Launch Week 6 is here

Bonjour Wasp connoisseurs 🐝 👋,

It's been a while, but we're back! We've been busy wasps and put our antennae down to work to deliver you v0.14, which is happening in exactly two days, on July 17th!

It's happening!

To reserve your spot (we can fit only so many people in our Discord), click here.

Join the kick-off event

Once you see the invite, mark yourself as "Interested," and that's it (don't make us talk to ourselves again)! Also, if you thought you'd slip by without a bad joke, you were direly wrong:

Why do wasps never leave tips? Because they are stingy.

🤯🤯🤯

Okay, now to the fun stuff—let's see what we're packing into this upcoming release!

#1: 📝 Define your data models in a separate prisma.schema file!

This change marks the beginning of one of our most requested features - splitting your Wasp config into multiple files! We know that as you develop your Wasp app and it grows, it can become unwieldy to have everything in one hefty .wasp file.

Define data models in prisma.schema file

That's why we started by extracting the data model definitions into a standalone Prisma schema file! This will significantly reduce the size of the Wasp config file and also allow for a more streamlined experience of writing PSL (Prisma Schema Language), with all the goodies like syntax highlighting and auto-completion working out of the box 🎉.

#2: 🔒 Auth Hooks - onBeforeSignup, onAfterSignup, and more!

Auth lifecycle hooks

Although Wasp's Auth feature is probably the fastest way to get authentication running in your full-stack app, adding your custom logic to the auth process can also be quite handy—e.g. if you want to log something, do some extra config etc.

We've made that easy, by offering several authentication lifecycle hooks that you can use for the exact purpose!

#3: 🆕 New authentication provider: Discord!

Discord as a new auth provider

This one is pretty self-explanatory, but that doesn't make it any less cool! Besides Google and GitHub, Discord is now a third social auth method natively supported by Wasp, next to Google and GitHub - that means all you need to do is define a single line in your Wasp config, and voilà - your users can now sign in with Discord!

#4: 👀 TypeScript SDK RFC - a sneak peek!

TS SDK proposal of code

As you might have seen in the community, this has been an ongoing topic of discussion for a while. Although having a dedicated configuration language (DSL) allows for the maximum customizability of the DX, having Wasp config in TypeScript instead will help out with language tooling (IDE syntax highlighting and auto-completion). Also, it might feel even more familiar to developers using Wasp.

This is why we decided to test the waters and see how we (and you) like it! We are still working out what it will look like, and we have laid out some of the ideas in this RFC for TS SDK. We'd love to hear from you and get your comments and ideas in this GitHub issue (or just come to our Discord and bash us there 😅)

#5: 🤯 OpenSaaS, Reloaded!

Open SaaS banner

And finally, the star of the last launch week, Open SaaS, a 100% free and open-source boilerplate starter for React & Node.js, powered by Wasp, has received its first makeover! You gave us a ton of amazing feedback and ideas, and we listened. Here's what's new:

  • Simplified Stripe payment logic
  • The code is now organized vertically by features (instead of client and server folders)
  • Introduced e2e tests with Playwright
  • Added an optional cookie consent banner that hooks up to Google Analytics
  • Bunch of small bug fixes and docs updates

And more! Don't forget that Lambo you will earn with your SaaS is just within arm's reach (well, it depends on how long your arm is). All you have to do is go to OpenSaaS and finally start that app you've been dreaming about 🏎️.

#6: 🫵 See you there!

That's pretty much it—we've given you a taste of what's coming, but for the real deal, you'll have to join us on Wednesday! Register here and make sure to mark yourself as interested—we'll see you there if you don't see us first (sorry)!

Join the kick-off event

Register for the kick-off event here.

Stay in the loop

dont leave

Every day, we'll update this page with the latest announcement of the day - to stay in the loop, follow us on Twitter/X and join our Discord - see you there!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app.html b/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app.html new file mode 100644 index 0000000000..183237e15d --- /dev/null +++ b/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app.html @@ -0,0 +1,32 @@ + + + + + +How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide | Wasp + + + + + + + + + + + + + + + + + + + +
+

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

· 16 min read
Lucas Lima do Nascimento

How to Add Auth to Your App

Although authentication is one of the most common web app features, there are so many different ways to go about it, which makes it a very non-trivial task. In this post, I will share my personal experience using Lucia - a modern, framework-agnostic authentication library that has been getting, deservedly so, a lot of love from the community in recent months.

First, I will demonstrate how you can implement it within your Next.js application through a step-by-step guide you can follow. It will require a fair amount of code and configuration, but the process itself is quite straightforward.

Secondly, we’ll see how to achieve the same with Wasp in just a few lines of code. Wasp is a batteries-included, full-stack framework for React & Node.js that uses Lucia under the hood to implement authentication. It runs fully on your infrastructure and is 100% open-source and free.

auth with Wasp

Why Lucia?

When it comes to adding authentication to your applications, there are several popular solutions available. For instance, Clerk offers a paid service, while NextAuth.js is an open-source solution alongside Lucia, which has become quite popular recently.

These tools provide robust features, but committing to third-party services — which not only adds another layer of complexity but also have paid tiers you have to keep an eye on — might be an overkill for a small project. In-house solutions keep things centralized but leave it to a developer to implement some of the mentioned features.

In our case, Lucia has proved to be a perfect middle ground - it’s not a third-party service and does not require a dedicated infrastructure, but it also provides a very solid foundation that’s easy to build upon.

Now, let’s dive into a step-by-step guide on how to implement your own authentication with Next.js and Lucia.

Step 1: Setting up Next.js

First, create a new Next.js project:

npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm install

Step 2: Install Lucia

Next, install Lucia:

npm install lucia

Step 3: Set up Authentication

Create an auth file in your project and add the necessary files for Lucia to be imported and initialized. It has a bunch of adapters for different databases, and you can check them all here. In this example, we’re going to use SQLite:

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";

const adapter = new BetterSQLite3Adapter(db); // your adapter

export const lucia = new Lucia(adapter, {
sessionCookie: {
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
// set to `true` when using HTTPS
secure: process.env.NODE_ENV === "production"
}
}
});

// To get some good Typescript support, add this!
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
}

Step 4: Add User to DB

Let’s add a database file to contain our schemas for now:

// lib/db.ts
import sqlite from "better-sqlite3";

export const db = sqlite("main.db");

db.exec(`CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY,
github_id INTEGER UNIQUE,
username TEXT NOT NULL
)`);

db.exec(`CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id)
)`);

export interface DatabaseUser {
id: string;
username: string;
github_id: number;
}

Step 5: Implement Login and Signup

To make this happen, we firstly have to create a GitHub OAuth app. This is relatively simple, you create it, add the necessary ENVs and callback URLs into your application and you’re good to go. You can follow GitHub docs to check how to do that.

//.env.local
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

After that, it’s a matter of adding login and signup functionalities to your pages, so, let’s do that real quick:

// login/page.tsx
import { validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function Page() {
const { user } = await validateRequest();
if (user) {
return redirect("/");
}
return (
<>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</>
);
}

After adding the page, we also have to add the login redirect to GitHub and the callback that’s going to be called. Let’s first add the login redirect with the authorization URL:

// login/github/route.ts
import { generateState } from "arctic";
import { github } from "../../../lib/auth";
import { cookies } from "next/headers";

export async function GET(): Promise<Response> {
const state = generateState();
const url = await github.createAuthorizationURL(state);

cookies().set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});

return Response.redirect(url);
}

And finally, the callback (which is what we actually add in GitHub OAuth):

// login/github/callback/route.ts
import { github, lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { cookies } from "next/headers";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";

import type { DatabaseUser } from "@/lib/db";

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const storedState = cookies().get("github_oauth_state")?.value ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400
});
}

try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const githubUser: GitHubUser = await githubUserResponse.json();
const existingUser = db.prepare("SELECT * FROM user WHERE github_id = ?").get(githubUser.id) as
| DatabaseUser
| undefined;

if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}

const userId = generateId(15);
db.prepare("INSERT INTO user (id, github_id, username) VALUES (?, ?, ?)").run(
userId,
githubUser.id,
githubUser.login
);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
} catch (e) {
if (e instanceof OAuth2RequestError && e.message === "bad_verification_code") {
// invalid code
return new Response(null, {
status: 400
});
}
return new Response(null, {
status: 500
});
}
}

interface GitHubUser {
id: string;
login: string;
}

Other important thing here is that, now, we’re going with GitHub OAuth, but, generally, these libraries contain a bunch of different login providers (including simple username and password), so it’s usually just a pick and choose if you want to add other providers.

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { cookies } from "next/headers";
import { cache } from "react";
import { GitHub } from "arctic";

import type { Session, User } from "lucia";
import type { DatabaseUser } from "./db";

// these two lines here might be important if you have node.js 18 or lower.
// you can check Lucia's documentation in more detail if that's the case
// (https://lucia-auth.com/getting-started/nextjs-app#polyfill)
// import { webcrypto } from "crypto";
// globalThis.crypto = webcrypto as Crypto;

const adapter = new BetterSqlite3Adapter(db, {
user: "user",
session: "session"
});

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production"
}
},
getUserAttributes: (attributes) => {
return {
githubId: attributes.github_id,
username: attributes.username
};
}
});

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: Omit<DatabaseUser, "id">;
}
}

export const validateRequest = cache(
async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null
};
}

const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session && result.session.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result;
}
);

export const github = new GitHub(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!);

Step 6: Protect Routes

After adding all that stuff to make the login properly work, we just have to ensure that routes are protected by checking authentication status — in this case, this is a simple page that shows username, id and a button in case signed in, and redirects to /login, where the user will complete the login above through a form.

import { lucia, validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";

export default async function Page() {
const { user } = await validateRequest();
if (!user) {
return redirect("/login");
}
return (
<>
<h1>Hi, {user.username}!</h1>
<p>Your user ID is {user.id}.</p>
<form action={logout}>
<button>Sign out</button>
</form>
</>
);
}

async function logout(): Promise<ActionResult> {
"use server";
const { session } = await validateRequest();
if (!session) {
return {
error: "Unauthorized"
};
}

await lucia.invalidateSession(session.id);

const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect("/login");
}

interface ActionResult {
error: string | null;
}

Piece of cake, isn’t it? Well, not really.

Let’s recap which steps were necessary to actually make this happen:

  • Set up your app.
  • Add Lucia.
  • Set up authentication.
  • Add User to DB.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Create some util functions.
  • Add Login and Sign up routes, with custom made components.
  • Finally, create a protected route.

https://media2.giphy.com/media/3ofSBnYbEPePeigIMg/giphy.gif?cid=7941fdc6x77sivlvr6hs2yu5aztvwjvhgugv6b718mjanr2h&ep=v1_gifs_search&rid=giphy.gif&ct=g

Honestly, when trying to create something cool FAST, repeating these steps and debugging a few logical problems here and there that always occur can feel a little bit frustrating. Soon, we’ll take a look at Wasp’s approach to solving that same problem and we’ll be able to compare how much easier Wasp’s auth implementation process is.

In case you want to check the whole code for this part, Lucia has an example repo (that is the source of most of the code shown), so, you can check it out if you’d like.

Wasp Implementation

Now, let’s go through how we can achieve the same things with Wasp 🐝. Although it still uses Lucia in the background, Wasp takes care of all the heavy-lifting for you, making the process much quicker and simpler. Let’s check out the developer experience for ourselves.

Before we just into it, in case you’re more of a visual learner, here’s a 1-minute video showcasing auth with wasp.

As seen in the video, Wasp is a framework for building apps with the benefits of using a configuration file to make development easier. It handles many repetitive tasks, allowing you to focus on creating unique features. In this tutorial, we’ll also learn more about the Wasp config file and see how it makes setting up authentication simpler.

Step 1: Create a Wasp Project

curl -sSL https://get.wasp-lang.dev/installer.sh | sh
wasp new my-wasp-app
cd my-wasp-app

Step 2: Add the User entity into our DB

As simple as defining the app.auth.userEntity entity in the schema.prisma file and running some migrations:

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
// Add your own fields below
// ...
}

Step 3: Define Authentication

In your main Wasp configuration, add the authentication provider you want for your app

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

And after that, just run in your terminal:

wasp db migrate-dev

Step 4: Get your GitHub OAuth credentials and app running

This part is similar for both frameworks, you can follow the documentation GitHub provides here to do so: Creating an OAuth app - GitHub Docs. For wasp app, the callback urls are:

  • While developing: http://localhost:3001/auth/github/callback
  • After deploying: https://your-server-url.com/auth/github/callback

After that, get your secrets and add it to the env file:

//.env.server
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

Step 5: Add the routes and pages

Now, let’s simply add some routing and the page necessary for login — the process is way easier since Wasp has pre-built Login and Signup Forms, we can simply add those directly:

// main.wasp

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@src/SignupPage"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@src/LoginPage"
}
// src/LoginPage.jsx
import { Link } from 'react-router-dom'
import { LoginForm } from 'wasp/client/auth'

export const LoginPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
// src/SignupPage.jsx
import { Link } from 'react-router-dom'
import { SignupForm } from 'wasp/client/auth'

export const SignupPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}

And finally, for protecting routes, is as simple as changing it in main.wasp adding authRequired: true , so, we can simply add it like this:

// main.wasp
page MainPage {
component: import Main from "@src/pages/Main",
authRequired: true
}

If you’d like to check this example in more depth, feel free to check this repo here: wasp/examples/todo-typescript at release · wasp-lang/wasp (github.com). +Other great place to check is their documentation, which can be found here. It covers most of what I said here, and even more (e.g. the awesome new hooks that came with Wasp v0.14)

https://media4.giphy.com/media/nDSlfqf0gn5g4/giphy.gif?cid=7941fdc6oxsddr7p8rjsuavcyq7ugiad8iqdu1ei25urcge4&ep=v1_gifs_search&rid=giphy.gif&ct=g

Way easier, isn’t it? Let’s review the steps we took to get here:

  • Set up the project.
  • Add the User entity to the database.
  • Define authentication in the main Wasp configuration.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Add routes and pages for login and signup with pre-built, easy-to-use components.
  • Protect routes by specifying authRequired in your configuration.

Customizing Wasp Auth

If you need more control and customization over the authentication flow, Wasp provides Auth hooks that allow you to tailor the experience to your app's specific needs. These hooks enable you to execute custom code during various stages of the authentication process, ensuring that you can implement any required custom behavior.

For more detailed information on using Auth hooks with Wasp, visit the Wasp documentation.

Bonus Section: Adding Email/Password Login with Wasp and Customizing Auth

Now let’s imagine we want to add email and password authentication — with all the usual features we’d expect that would follow this login method (e.g. reset password, email verification, etc.).

With Wasp, all we have to do is add a few lines to your main.wasp file, so, simply updating your Wasp configuration to include email/password authentication makes it work straight out of the box!

https://wasp-lang.dev/img/auth-ui/auth-demo-compiler.gif

Wasp will handle the rest, also updating UI components and ensuring a smooth and secure authentication flow.

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
// 1. Specify the User entity
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {},
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options
emailVerification: {
clientRoute: EmailVerificationRoute, //this route/page should be created
},
passwordReset: {
clientRoute: PasswordResetRoute, //this route/page should be created
},
// Add an emailSender -- Dummy just logs to console for dev purposes
// but there are a ton of supported providers :D
emailSender: {
provider: Dummy,
},
},
},
onAuthFailedRedirectTo: "/login"
},
}

Implementing this in Next.js with Lucia would take a lot more work, involving a bunch of different stuff from actually sending the emails, to generating the verification tokens and more. They reference this here, but again, Wasp’s Auth makes the whole process way easier, handling a bunch of the complexity for us while also giving a bunch of other UI components, ready to use, to ease the UI details (e.g. VerifyEmailForm, ForgotPasswordForm and, ResetPasswordForm).

The whole point here is the difference in time and developer experience in order to implement the same scenarios. For the Next.js project with Lucia, you will spend at least a few hours implementing everything if you’re going all by yourself. That same experience translates to no more than 1 hour with Wasp. What to do with the rest of the time? Implement the important stuff your particular business requires!

Can you show us your support?

https://media2.giphy.com/media/l0MYAs5E2oIDCq9So/giphy.gif?cid=7941fdc6l6i66eq1dc7i5rz05nkl4mgjltyv206syb0o304g&ep=v1_gifs_search&rid=giphy.gif&ct=g

Are you interested in more content like this? Sign up for our newsletter and give us a star on GitHub! We need your support to keep pushing our projects forward 😀

Conclusion

https://media2.giphy.com/media/l1AsKaVNyNXHKUkUw/giphy.gif?cid=7941fdc6u6vp4j2gpjfuizupxlvfdzskl03ncci2e7jq17zr&ep=v1_gifs_search&rid=giphy.gif&ct=g

I think that if you’re a developer who wants to get things done, you probably noted the significant difference in complexity levels of both of those implementations.

By reducing boilerplate and abstracting repetitive tasks, Wasp allows developers to focus more on building unique features rather than getting bogged down by authentication details. This can be especially beneficial for small teams or individual developers aiming to launch products quickly.

Of course, generally when we talk abstractions, it always comes with the downside of losing the finesse of a more personal implementation. In this case, Wasp provides a bunch of stuff for you to implement around and uses Lucia on the background, so the scenario where there’s a mismatch of content implementation is highly unlikable to happen.

In summary, while implementing your own authentication with Next.js and Lucia provides complete control and customization, it can be complex and time-consuming. On the other hand, using a solution like Wasp simplifies the process, reduces code length, and speeds up development.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + + + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index 51d50953d5..76bc6d6fda 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -19,13 +19,13 @@ - - + +
-

Archive

Archive

2022

2023

- - +

Archive

Archive

2022

2023

+ + \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml index 074ac022b1..9a08c92f17 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -2,11 +2,29 @@ https://wasp-lang.dev/blog Wasp Blog - 2024-07-15T00:00:00.000Z + 2024-08-13T00:00:00.000Z https://github.com/jpmonette/feed Wasp Blog https://wasp-lang.dev/img/favicon.ico + + <![CDATA[How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide]]> + https://wasp-lang.dev/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app + + 2024-08-13T00:00:00.000Z + +

How to Add Auth to Your App

Although authentication is one of the most common web app features, there are so many different ways to go about it, which makes it a very non-trivial task. In this post, I will share my personal experience using Lucia - a modern, framework-agnostic authentication library that has been getting, deservedly so, a lot of love from the community in recent months.

First, I will demonstrate how you can implement it within your Next.js application through a step-by-step guide you can follow. It will require a fair amount of code and configuration, but the process itself is quite straightforward.

Secondly, we’ll see how to achieve the same with Wasp in just a few lines of code. Wasp is a batteries-included, full-stack framework for React & Node.js that uses Lucia under the hood to implement authentication. It runs fully on your infrastructure and is 100% open-source and free.

auth with Wasp

Why Lucia?

When it comes to adding authentication to your applications, there are several popular solutions available. For instance, Clerk offers a paid service, while NextAuth.js is an open-source solution alongside Lucia, which has become quite popular recently.

These tools provide robust features, but committing to third-party services — which not only adds another layer of complexity but also have paid tiers you have to keep an eye on — might be an overkill for a small project. In-house solutions keep things centralized but leave it to a developer to implement some of the mentioned features.

In our case, Lucia has proved to be a perfect middle ground - it’s not a third-party service and does not require a dedicated infrastructure, but it also provides a very solid foundation that’s easy to build upon.

Now, let’s dive into a step-by-step guide on how to implement your own authentication with Next.js and Lucia.

Step 1: Setting up Next.js

First, create a new Next.js project:

npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm install

Step 2: Install Lucia

Next, install Lucia:

npm install lucia

Step 3: Set up Authentication

Create an auth file in your project and add the necessary files for Lucia to be imported and initialized. It has a bunch of adapters for different databases, and you can check them all here. In this example, we’re going to use SQLite:

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";

const adapter = new BetterSQLite3Adapter(db); // your adapter

export const lucia = new Lucia(adapter, {
sessionCookie: {
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
// set to `true` when using HTTPS
secure: process.env.NODE_ENV === "production"
}
}
});

// To get some good Typescript support, add this!
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
}

Step 4: Add User to DB

Let’s add a database file to contain our schemas for now:

// lib/db.ts
import sqlite from "better-sqlite3";

export const db = sqlite("main.db");

db.exec(`CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY,
github_id INTEGER UNIQUE,
username TEXT NOT NULL
)`);

db.exec(`CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id)
)`);

export interface DatabaseUser {
id: string;
username: string;
github_id: number;
}

Step 5: Implement Login and Signup

To make this happen, we firstly have to create a GitHub OAuth app. This is relatively simple, you create it, add the necessary ENVs and callback URLs into your application and you’re good to go. You can follow GitHub docs to check how to do that.

//.env.local
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

After that, it’s a matter of adding login and signup functionalities to your pages, so, let’s do that real quick:

// login/page.tsx
import { validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function Page() {
const { user } = await validateRequest();
if (user) {
return redirect("/");
}
return (
<>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</>
);
}

After adding the page, we also have to add the login redirect to GitHub and the callback that’s going to be called. Let’s first add the login redirect with the authorization URL:

// login/github/route.ts
import { generateState } from "arctic";
import { github } from "../../../lib/auth";
import { cookies } from "next/headers";

export async function GET(): Promise<Response> {
const state = generateState();
const url = await github.createAuthorizationURL(state);

cookies().set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});

return Response.redirect(url);
}

And finally, the callback (which is what we actually add in GitHub OAuth):

// login/github/callback/route.ts
import { github, lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { cookies } from "next/headers";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";

import type { DatabaseUser } from "@/lib/db";

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const storedState = cookies().get("github_oauth_state")?.value ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400
});
}

try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const githubUser: GitHubUser = await githubUserResponse.json();
const existingUser = db.prepare("SELECT * FROM user WHERE github_id = ?").get(githubUser.id) as
| DatabaseUser
| undefined;

if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}

const userId = generateId(15);
db.prepare("INSERT INTO user (id, github_id, username) VALUES (?, ?, ?)").run(
userId,
githubUser.id,
githubUser.login
);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
} catch (e) {
if (e instanceof OAuth2RequestError && e.message === "bad_verification_code") {
// invalid code
return new Response(null, {
status: 400
});
}
return new Response(null, {
status: 500
});
}
}

interface GitHubUser {
id: string;
login: string;
}

Other important thing here is that, now, we’re going with GitHub OAuth, but, generally, these libraries contain a bunch of different login providers (including simple username and password), so it’s usually just a pick and choose if you want to add other providers.

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { cookies } from "next/headers";
import { cache } from "react";
import { GitHub } from "arctic";

import type { Session, User } from "lucia";
import type { DatabaseUser } from "./db";

// these two lines here might be important if you have node.js 18 or lower.
// you can check Lucia's documentation in more detail if that's the case
// (https://lucia-auth.com/getting-started/nextjs-app#polyfill)
// import { webcrypto } from "crypto";
// globalThis.crypto = webcrypto as Crypto;

const adapter = new BetterSqlite3Adapter(db, {
user: "user",
session: "session"
});

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production"
}
},
getUserAttributes: (attributes) => {
return {
githubId: attributes.github_id,
username: attributes.username
};
}
});

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: Omit<DatabaseUser, "id">;
}
}

export const validateRequest = cache(
async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null
};
}

const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session && result.session.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result;
}
);

export const github = new GitHub(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!);

Step 6: Protect Routes

After adding all that stuff to make the login properly work, we just have to ensure that routes are protected by checking authentication status — in this case, this is a simple page that shows username, id and a button in case signed in, and redirects to /login, where the user will complete the login above through a form.

import { lucia, validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";

export default async function Page() {
const { user } = await validateRequest();
if (!user) {
return redirect("/login");
}
return (
<>
<h1>Hi, {user.username}!</h1>
<p>Your user ID is {user.id}.</p>
<form action={logout}>
<button>Sign out</button>
</form>
</>
);
}

async function logout(): Promise<ActionResult> {
"use server";
const { session } = await validateRequest();
if (!session) {
return {
error: "Unauthorized"
};
}

await lucia.invalidateSession(session.id);

const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect("/login");
}

interface ActionResult {
error: string | null;
}

Piece of cake, isn’t it? Well, not really.

Let’s recap which steps were necessary to actually make this happen:

  • Set up your app.
  • Add Lucia.
  • Set up authentication.
  • Add User to DB.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Create some util functions.
  • Add Login and Sign up routes, with custom made components.
  • Finally, create a protected route.

https://media2.giphy.com/media/3ofSBnYbEPePeigIMg/giphy.gif?cid=7941fdc6x77sivlvr6hs2yu5aztvwjvhgugv6b718mjanr2h&ep=v1_gifs_search&rid=giphy.gif&ct=g

Honestly, when trying to create something cool FAST, repeating these steps and debugging a few logical problems here and there that always occur can feel a little bit frustrating. Soon, we’ll take a look at Wasp’s approach to solving that same problem and we’ll be able to compare how much easier Wasp’s auth implementation process is.

In case you want to check the whole code for this part, Lucia has an example repo (that is the source of most of the code shown), so, you can check it out if you’d like.

Wasp Implementation

Now, let’s go through how we can achieve the same things with Wasp 🐝. Although it still uses Lucia in the background, Wasp takes care of all the heavy-lifting for you, making the process much quicker and simpler. Let’s check out the developer experience for ourselves.

Before we just into it, in case you’re more of a visual learner, here’s a 1-minute video showcasing auth with wasp.

As seen in the video, Wasp is a framework for building apps with the benefits of using a configuration file to make development easier. It handles many repetitive tasks, allowing you to focus on creating unique features. In this tutorial, we’ll also learn more about the Wasp config file and see how it makes setting up authentication simpler.

Step 1: Create a Wasp Project

curl -sSL https://get.wasp-lang.dev/installer.sh | sh
wasp new my-wasp-app
cd my-wasp-app

Step 2: Add the User entity into our DB

As simple as defining the app.auth.userEntity entity in the schema.prisma file and running some migrations:

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
// Add your own fields below
// ...
}

Step 3: Define Authentication

In your main Wasp configuration, add the authentication provider you want for your app

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

And after that, just run in your terminal:

wasp db migrate-dev

Step 4: Get your GitHub OAuth credentials and app running

This part is similar for both frameworks, you can follow the documentation GitHub provides here to do so: Creating an OAuth app - GitHub Docs. For wasp app, the callback urls are:

  • While developing: http://localhost:3001/auth/github/callback
  • After deploying: https://your-server-url.com/auth/github/callback

After that, get your secrets and add it to the env file:

//.env.server
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

Step 5: Add the routes and pages

Now, let’s simply add some routing and the page necessary for login — the process is way easier since Wasp has pre-built Login and Signup Forms, we can simply add those directly:

// main.wasp

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@src/SignupPage"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@src/LoginPage"
}
// src/LoginPage.jsx
import { Link } from 'react-router-dom'
import { LoginForm } from 'wasp/client/auth'

export const LoginPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
// src/SignupPage.jsx
import { Link } from 'react-router-dom'
import { SignupForm } from 'wasp/client/auth'

export const SignupPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}

And finally, for protecting routes, is as simple as changing it in main.wasp adding authRequired: true , so, we can simply add it like this:

// main.wasp
page MainPage {
component: import Main from "@src/pages/Main",
authRequired: true
}

If you’d like to check this example in more depth, feel free to check this repo here: wasp/examples/todo-typescript at release · wasp-lang/wasp (github.com). +Other great place to check is their documentation, which can be found here. It covers most of what I said here, and even more (e.g. the awesome new hooks that came with Wasp v0.14)

https://media4.giphy.com/media/nDSlfqf0gn5g4/giphy.gif?cid=7941fdc6oxsddr7p8rjsuavcyq7ugiad8iqdu1ei25urcge4&ep=v1_gifs_search&rid=giphy.gif&ct=g

Way easier, isn’t it? Let’s review the steps we took to get here:

  • Set up the project.
  • Add the User entity to the database.
  • Define authentication in the main Wasp configuration.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Add routes and pages for login and signup with pre-built, easy-to-use components.
  • Protect routes by specifying authRequired in your configuration.

Customizing Wasp Auth

If you need more control and customization over the authentication flow, Wasp provides Auth hooks that allow you to tailor the experience to your app's specific needs. These hooks enable you to execute custom code during various stages of the authentication process, ensuring that you can implement any required custom behavior.

For more detailed information on using Auth hooks with Wasp, visit the Wasp documentation.

Bonus Section: Adding Email/Password Login with Wasp and Customizing Auth

Now let’s imagine we want to add email and password authentication — with all the usual features we’d expect that would follow this login method (e.g. reset password, email verification, etc.).

With Wasp, all we have to do is add a few lines to your main.wasp file, so, simply updating your Wasp configuration to include email/password authentication makes it work straight out of the box!

https://wasp-lang.dev/img/auth-ui/auth-demo-compiler.gif

Wasp will handle the rest, also updating UI components and ensuring a smooth and secure authentication flow.

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
// 1. Specify the User entity
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {},
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options
emailVerification: {
clientRoute: EmailVerificationRoute, //this route/page should be created
},
passwordReset: {
clientRoute: PasswordResetRoute, //this route/page should be created
},
// Add an emailSender -- Dummy just logs to console for dev purposes
// but there are a ton of supported providers :D
emailSender: {
provider: Dummy,
},
},
},
onAuthFailedRedirectTo: "/login"
},
}

Implementing this in Next.js with Lucia would take a lot more work, involving a bunch of different stuff from actually sending the emails, to generating the verification tokens and more. They reference this here, but again, Wasp’s Auth makes the whole process way easier, handling a bunch of the complexity for us while also giving a bunch of other UI components, ready to use, to ease the UI details (e.g. VerifyEmailForm, ForgotPasswordForm and, ResetPasswordForm).

The whole point here is the difference in time and developer experience in order to implement the same scenarios. For the Next.js project with Lucia, you will spend at least a few hours implementing everything if you’re going all by yourself. That same experience translates to no more than 1 hour with Wasp. What to do with the rest of the time? Implement the important stuff your particular business requires!

Can you show us your support?

https://media2.giphy.com/media/l0MYAs5E2oIDCq9So/giphy.gif?cid=7941fdc6l6i66eq1dc7i5rz05nkl4mgjltyv206syb0o304g&ep=v1_gifs_search&rid=giphy.gif&ct=g

Are you interested in more content like this? Sign up for our newsletter and give us a star on GitHub! We need your support to keep pushing our projects forward 😀

Conclusion

https://media2.giphy.com/media/l1AsKaVNyNXHKUkUw/giphy.gif?cid=7941fdc6u6vp4j2gpjfuizupxlvfdzskl03ncci2e7jq17zr&ep=v1_gifs_search&rid=giphy.gif&ct=g

I think that if you’re a developer who wants to get things done, you probably noted the significant difference in complexity levels of both of those implementations.

By reducing boilerplate and abstracting repetitive tasks, Wasp allows developers to focus more on building unique features rather than getting bogged down by authentication details. This can be especially beneficial for small teams or individual developers aiming to launch products quickly.

Of course, generally when we talk abstractions, it always comes with the downside of losing the finesse of a more personal implementation. In this case, Wasp provides a bunch of stuff for you to implement around and uses Lucia on the background, so the scenario where there’s a mismatch of content implementation is highly unlikable to happen.

In summary, while implementing your own authentication with Next.js and Lucia provides complete control and customization, it can be complex and time-consuming. On the other hand, using a solution like Wasp simplifies the process, reduces code length, and speeds up development.

]]> + + Lucas Lima do Nascimento + https://github.com/LLxD + + + + + + + <![CDATA[Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩]]> https://wasp-lang.dev/blog/2024/07/15/wasp-launch-week-six diff --git a/blog/rss.xml b/blog/rss.xml index a2b7f695c0..e3258c6d61 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -4,10 +4,24 @@ Wasp Blog https://wasp-lang.dev/blog Wasp Blog - Mon, 15 Jul 2024 00:00:00 GMT + Tue, 13 Aug 2024 00:00:00 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en + + <![CDATA[How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide]]> + https://wasp-lang.dev/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app + https://wasp-lang.dev/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-app + Tue, 13 Aug 2024 00:00:00 GMT + +

How to Add Auth to Your App

Although authentication is one of the most common web app features, there are so many different ways to go about it, which makes it a very non-trivial task. In this post, I will share my personal experience using Lucia - a modern, framework-agnostic authentication library that has been getting, deservedly so, a lot of love from the community in recent months.

First, I will demonstrate how you can implement it within your Next.js application through a step-by-step guide you can follow. It will require a fair amount of code and configuration, but the process itself is quite straightforward.

Secondly, we’ll see how to achieve the same with Wasp in just a few lines of code. Wasp is a batteries-included, full-stack framework for React & Node.js that uses Lucia under the hood to implement authentication. It runs fully on your infrastructure and is 100% open-source and free.

auth with Wasp

Why Lucia?

When it comes to adding authentication to your applications, there are several popular solutions available. For instance, Clerk offers a paid service, while NextAuth.js is an open-source solution alongside Lucia, which has become quite popular recently.

These tools provide robust features, but committing to third-party services — which not only adds another layer of complexity but also have paid tiers you have to keep an eye on — might be an overkill for a small project. In-house solutions keep things centralized but leave it to a developer to implement some of the mentioned features.

In our case, Lucia has proved to be a perfect middle ground - it’s not a third-party service and does not require a dedicated infrastructure, but it also provides a very solid foundation that’s easy to build upon.

Now, let’s dive into a step-by-step guide on how to implement your own authentication with Next.js and Lucia.

Step 1: Setting up Next.js

First, create a new Next.js project:

npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm install

Step 2: Install Lucia

Next, install Lucia:

npm install lucia

Step 3: Set up Authentication

Create an auth file in your project and add the necessary files for Lucia to be imported and initialized. It has a bunch of adapters for different databases, and you can check them all here. In this example, we’re going to use SQLite:

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";

const adapter = new BetterSQLite3Adapter(db); // your adapter

export const lucia = new Lucia(adapter, {
sessionCookie: {
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
// set to `true` when using HTTPS
secure: process.env.NODE_ENV === "production"
}
}
});

// To get some good Typescript support, add this!
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
}

Step 4: Add User to DB

Let’s add a database file to contain our schemas for now:

// lib/db.ts
import sqlite from "better-sqlite3";

export const db = sqlite("main.db");

db.exec(`CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY,
github_id INTEGER UNIQUE,
username TEXT NOT NULL
)`);

db.exec(`CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id)
)`);

export interface DatabaseUser {
id: string;
username: string;
github_id: number;
}

Step 5: Implement Login and Signup

To make this happen, we firstly have to create a GitHub OAuth app. This is relatively simple, you create it, add the necessary ENVs and callback URLs into your application and you’re good to go. You can follow GitHub docs to check how to do that.

//.env.local
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

After that, it’s a matter of adding login and signup functionalities to your pages, so, let’s do that real quick:

// login/page.tsx
import { validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function Page() {
const { user } = await validateRequest();
if (user) {
return redirect("/");
}
return (
<>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</>
);
}

After adding the page, we also have to add the login redirect to GitHub and the callback that’s going to be called. Let’s first add the login redirect with the authorization URL:

// login/github/route.ts
import { generateState } from "arctic";
import { github } from "../../../lib/auth";
import { cookies } from "next/headers";

export async function GET(): Promise<Response> {
const state = generateState();
const url = await github.createAuthorizationURL(state);

cookies().set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});

return Response.redirect(url);
}

And finally, the callback (which is what we actually add in GitHub OAuth):

// login/github/callback/route.ts
import { github, lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { cookies } from "next/headers";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";

import type { DatabaseUser } from "@/lib/db";

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const storedState = cookies().get("github_oauth_state")?.value ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400
});
}

try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const githubUser: GitHubUser = await githubUserResponse.json();
const existingUser = db.prepare("SELECT * FROM user WHERE github_id = ?").get(githubUser.id) as
| DatabaseUser
| undefined;

if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}

const userId = generateId(15);
db.prepare("INSERT INTO user (id, github_id, username) VALUES (?, ?, ?)").run(
userId,
githubUser.id,
githubUser.login
);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
} catch (e) {
if (e instanceof OAuth2RequestError && e.message === "bad_verification_code") {
// invalid code
return new Response(null, {
status: 400
});
}
return new Response(null, {
status: 500
});
}
}

interface GitHubUser {
id: string;
login: string;
}

Other important thing here is that, now, we’re going with GitHub OAuth, but, generally, these libraries contain a bunch of different login providers (including simple username and password), so it’s usually just a pick and choose if you want to add other providers.

// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { cookies } from "next/headers";
import { cache } from "react";
import { GitHub } from "arctic";

import type { Session, User } from "lucia";
import type { DatabaseUser } from "./db";

// these two lines here might be important if you have node.js 18 or lower.
// you can check Lucia's documentation in more detail if that's the case
// (https://lucia-auth.com/getting-started/nextjs-app#polyfill)
// import { webcrypto } from "crypto";
// globalThis.crypto = webcrypto as Crypto;

const adapter = new BetterSqlite3Adapter(db, {
user: "user",
session: "session"
});

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production"
}
},
getUserAttributes: (attributes) => {
return {
githubId: attributes.github_id,
username: attributes.username
};
}
});

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: Omit<DatabaseUser, "id">;
}
}

export const validateRequest = cache(
async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null
};
}

const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session && result.session.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result;
}
);

export const github = new GitHub(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!);

Step 6: Protect Routes

After adding all that stuff to make the login properly work, we just have to ensure that routes are protected by checking authentication status — in this case, this is a simple page that shows username, id and a button in case signed in, and redirects to /login, where the user will complete the login above through a form.

import { lucia, validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";

export default async function Page() {
const { user } = await validateRequest();
if (!user) {
return redirect("/login");
}
return (
<>
<h1>Hi, {user.username}!</h1>
<p>Your user ID is {user.id}.</p>
<form action={logout}>
<button>Sign out</button>
</form>
</>
);
}

async function logout(): Promise<ActionResult> {
"use server";
const { session } = await validateRequest();
if (!session) {
return {
error: "Unauthorized"
};
}

await lucia.invalidateSession(session.id);

const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect("/login");
}

interface ActionResult {
error: string | null;
}

Piece of cake, isn’t it? Well, not really.

Let’s recap which steps were necessary to actually make this happen:

  • Set up your app.
  • Add Lucia.
  • Set up authentication.
  • Add User to DB.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Create some util functions.
  • Add Login and Sign up routes, with custom made components.
  • Finally, create a protected route.

https://media2.giphy.com/media/3ofSBnYbEPePeigIMg/giphy.gif?cid=7941fdc6x77sivlvr6hs2yu5aztvwjvhgugv6b718mjanr2h&ep=v1_gifs_search&rid=giphy.gif&ct=g

Honestly, when trying to create something cool FAST, repeating these steps and debugging a few logical problems here and there that always occur can feel a little bit frustrating. Soon, we’ll take a look at Wasp’s approach to solving that same problem and we’ll be able to compare how much easier Wasp’s auth implementation process is.

In case you want to check the whole code for this part, Lucia has an example repo (that is the source of most of the code shown), so, you can check it out if you’d like.

Wasp Implementation

Now, let’s go through how we can achieve the same things with Wasp 🐝. Although it still uses Lucia in the background, Wasp takes care of all the heavy-lifting for you, making the process much quicker and simpler. Let’s check out the developer experience for ourselves.

Before we just into it, in case you’re more of a visual learner, here’s a 1-minute video showcasing auth with wasp.

As seen in the video, Wasp is a framework for building apps with the benefits of using a configuration file to make development easier. It handles many repetitive tasks, allowing you to focus on creating unique features. In this tutorial, we’ll also learn more about the Wasp config file and see how it makes setting up authentication simpler.

Step 1: Create a Wasp Project

curl -sSL https://get.wasp-lang.dev/installer.sh | sh
wasp new my-wasp-app
cd my-wasp-app

Step 2: Add the User entity into our DB

As simple as defining the app.auth.userEntity entity in the schema.prisma file and running some migrations:

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
// Add your own fields below
// ...
}

Step 3: Define Authentication

In your main Wasp configuration, add the authentication provider you want for your app

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

And after that, just run in your terminal:

wasp db migrate-dev

Step 4: Get your GitHub OAuth credentials and app running

This part is similar for both frameworks, you can follow the documentation GitHub provides here to do so: Creating an OAuth app - GitHub Docs. For wasp app, the callback urls are:

  • While developing: http://localhost:3001/auth/github/callback
  • After deploying: https://your-server-url.com/auth/github/callback

After that, get your secrets and add it to the env file:

//.env.server
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

Step 5: Add the routes and pages

Now, let’s simply add some routing and the page necessary for login — the process is way easier since Wasp has pre-built Login and Signup Forms, we can simply add those directly:

// main.wasp

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@src/SignupPage"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@src/LoginPage"
}
// src/LoginPage.jsx
import { Link } from 'react-router-dom'
import { LoginForm } from 'wasp/client/auth'

export const LoginPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
// src/SignupPage.jsx
import { Link } from 'react-router-dom'
import { SignupForm } from 'wasp/client/auth'

export const SignupPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}

And finally, for protecting routes, is as simple as changing it in main.wasp adding authRequired: true , so, we can simply add it like this:

// main.wasp
page MainPage {
component: import Main from "@src/pages/Main",
authRequired: true
}

If you’d like to check this example in more depth, feel free to check this repo here: wasp/examples/todo-typescript at release · wasp-lang/wasp (github.com). +Other great place to check is their documentation, which can be found here. It covers most of what I said here, and even more (e.g. the awesome new hooks that came with Wasp v0.14)

https://media4.giphy.com/media/nDSlfqf0gn5g4/giphy.gif?cid=7941fdc6oxsddr7p8rjsuavcyq7ugiad8iqdu1ei25urcge4&ep=v1_gifs_search&rid=giphy.gif&ct=g

Way easier, isn’t it? Let’s review the steps we took to get here:

  • Set up the project.
  • Add the User entity to the database.
  • Define authentication in the main Wasp configuration.
  • Obtain GitHub OAuth credentials and configure your environment variables.
  • Add routes and pages for login and signup with pre-built, easy-to-use components.
  • Protect routes by specifying authRequired in your configuration.

Customizing Wasp Auth

If you need more control and customization over the authentication flow, Wasp provides Auth hooks that allow you to tailor the experience to your app's specific needs. These hooks enable you to execute custom code during various stages of the authentication process, ensuring that you can implement any required custom behavior.

For more detailed information on using Auth hooks with Wasp, visit the Wasp documentation.

Bonus Section: Adding Email/Password Login with Wasp and Customizing Auth

Now let’s imagine we want to add email and password authentication — with all the usual features we’d expect that would follow this login method (e.g. reset password, email verification, etc.).

With Wasp, all we have to do is add a few lines to your main.wasp file, so, simply updating your Wasp configuration to include email/password authentication makes it work straight out of the box!

https://wasp-lang.dev/img/auth-ui/auth-demo-compiler.gif

Wasp will handle the rest, also updating UI components and ensuring a smooth and secure authentication flow.

//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
// 1. Specify the User entity
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {},
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options
emailVerification: {
clientRoute: EmailVerificationRoute, //this route/page should be created
},
passwordReset: {
clientRoute: PasswordResetRoute, //this route/page should be created
},
// Add an emailSender -- Dummy just logs to console for dev purposes
// but there are a ton of supported providers :D
emailSender: {
provider: Dummy,
},
},
},
onAuthFailedRedirectTo: "/login"
},
}

Implementing this in Next.js with Lucia would take a lot more work, involving a bunch of different stuff from actually sending the emails, to generating the verification tokens and more. They reference this here, but again, Wasp’s Auth makes the whole process way easier, handling a bunch of the complexity for us while also giving a bunch of other UI components, ready to use, to ease the UI details (e.g. VerifyEmailForm, ForgotPasswordForm and, ResetPasswordForm).

The whole point here is the difference in time and developer experience in order to implement the same scenarios. For the Next.js project with Lucia, you will spend at least a few hours implementing everything if you’re going all by yourself. That same experience translates to no more than 1 hour with Wasp. What to do with the rest of the time? Implement the important stuff your particular business requires!

Can you show us your support?

https://media2.giphy.com/media/l0MYAs5E2oIDCq9So/giphy.gif?cid=7941fdc6l6i66eq1dc7i5rz05nkl4mgjltyv206syb0o304g&ep=v1_gifs_search&rid=giphy.gif&ct=g

Are you interested in more content like this? Sign up for our newsletter and give us a star on GitHub! We need your support to keep pushing our projects forward 😀

Conclusion

https://media2.giphy.com/media/l1AsKaVNyNXHKUkUw/giphy.gif?cid=7941fdc6u6vp4j2gpjfuizupxlvfdzskl03ncci2e7jq17zr&ep=v1_gifs_search&rid=giphy.gif&ct=g

I think that if you’re a developer who wants to get things done, you probably noted the significant difference in complexity levels of both of those implementations.

By reducing boilerplate and abstracting repetitive tasks, Wasp allows developers to focus more on building unique features rather than getting bogged down by authentication details. This can be especially beneficial for small teams or individual developers aiming to launch products quickly.

Of course, generally when we talk abstractions, it always comes with the downside of losing the finesse of a more personal implementation. In this case, Wasp provides a bunch of stuff for you to implement around and uses Lucia on the background, so the scenario where there’s a mismatch of content implementation is highly unlikable to happen.

In summary, while implementing your own authentication with Next.js and Lucia provides complete control and customization, it can be complex and time-consuming. On the other hand, using a solution like Wasp simplifies the process, reduces code length, and speeds up development.

]]> + webdev + tech + react + nextjs + tutorial + <![CDATA[Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩]]> https://wasp-lang.dev/blog/2024/07/15/wasp-launch-week-six diff --git a/blog/tags.html b/blog/tags.html index c492eb368c..4bae38b86a 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -19,13 +19,13 @@ - - + +
-

Tags

- - +

Tags

+ + \ No newline at end of file diff --git a/blog/tags/acquire.html b/blog/tags/acquire.html index 708d0c1f0b..558a9cf6d8 100644 --- a/blog/tags/acquire.html +++ b/blog/tags/acquire.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "acquire"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
- - +

One post tagged with "acquire"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
+ + \ No newline at end of file diff --git a/blog/tags/agent.html b/blog/tags/agent.html index 8e7379df1a..d528c43d37 100644 --- a/blog/tags/agent.html +++ b/blog/tags/agent.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "agent"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

2 posts tagged with "agent"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/ai.html b/blog/tags/ai.html index b284d5b8a1..2c7e07c63d 100644 --- a/blog/tags/ai.html +++ b/blog/tags/ai.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "ai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

- - +

7 posts tagged with "ai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/auth.html b/blog/tags/auth.html index 27c5b4d04d..50978e5be9 100644 --- a/blog/tags/auth.html +++ b/blog/tags/auth.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "auth"

View All Tags
By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more
- - +

One post tagged with "auth"

View All Tags
By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more
+ + \ No newline at end of file diff --git a/blog/tags/boilerplate.html b/blog/tags/boilerplate.html index 016fc9546d..918b215df4 100644 --- a/blog/tags/boilerplate.html +++ b/blog/tags/boilerplate.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "boilerplate"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

- - +

2 posts tagged with "boilerplate"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/career.html b/blog/tags/career.html index 0692e43354..f8475eacbd 100644 --- a/blog/tags/career.html +++ b/blog/tags/career.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "career"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

- - +

2 posts tagged with "career"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/chakra.html b/blog/tags/chakra.html index 5e5743d73b..6142e200de 100644 --- a/blog/tags/chakra.html +++ b/blog/tags/chakra.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "chakra"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "chakra"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/chatgpt.html b/blog/tags/chatgpt.html index cd498721db..f495b74211 100644 --- a/blog/tags/chatgpt.html +++ b/blog/tags/chatgpt.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "chatgpt"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
- - +

One post tagged with "chatgpt"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
+ + \ No newline at end of file diff --git a/blog/tags/clean-code.html b/blog/tags/clean-code.html index fc52e72b29..6c296a31cf 100644 --- a/blog/tags/clean-code.html +++ b/blog/tags/clean-code.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "clean-code"

View All Tags
By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

- - +

2 posts tagged with "clean-code"

View All Tags
By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/css.html b/blog/tags/css.html index 6e398d7a08..00ec9ded26 100644 --- a/blog/tags/css.html +++ b/blog/tags/css.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "css"

View All Tags
By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more
- - +

One post tagged with "css"

View All Tags
By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more
+ + \ No newline at end of file diff --git a/blog/tags/database.html b/blog/tags/database.html index 5beb36c157..0606ee871f 100644 --- a/blog/tags/database.html +++ b/blog/tags/database.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "database"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
- - +

One post tagged with "database"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
+ + \ No newline at end of file diff --git a/blog/tags/discord.html b/blog/tags/discord.html index 9beed62618..948f73535c 100644 --- a/blog/tags/discord.html +++ b/blog/tags/discord.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "discord"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
- - +

One post tagged with "discord"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
+ + \ No newline at end of file diff --git a/blog/tags/express.html b/blog/tags/express.html index 97ccc8cad2..25c2a1499c 100644 --- a/blog/tags/express.html +++ b/blog/tags/express.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "express"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "express"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/blog/tags/feature.html b/blog/tags/feature.html index 093d24bcd1..fe80bc7d96 100644 --- a/blog/tags/feature.html +++ b/blog/tags/feature.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "feature"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

- - +

5 posts tagged with "feature"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/framework.html b/blog/tags/framework.html index 0f617cf577..eb92c1f285 100644 --- a/blog/tags/framework.html +++ b/blog/tags/framework.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "framework"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

- - +

2 posts tagged with "framework"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/full-stack.html b/blog/tags/full-stack.html index cc07c96782..fb714a1123 100644 --- a/blog/tags/full-stack.html +++ b/blog/tags/full-stack.html @@ -19,13 +19,13 @@ - - + +
-

4 posts tagged with "full-stack"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

- - +

4 posts tagged with "full-stack"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/fullstack.html b/blog/tags/fullstack.html index 16bae9bab7..3a419491cb 100644 --- a/blog/tags/fullstack.html +++ b/blog/tags/fullstack.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "fullstack"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

- - +

7 posts tagged with "fullstack"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/function-calling.html b/blog/tags/function-calling.html index 9862a623bb..1248cbe0c9 100644 --- a/blog/tags/function-calling.html +++ b/blog/tags/function-calling.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "function calling"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
- - +

One post tagged with "function calling"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
+ + \ No newline at end of file diff --git a/blog/tags/generate.html b/blog/tags/generate.html index 38f4aca94c..ceadc9bbbf 100644 --- a/blog/tags/generate.html +++ b/blog/tags/generate.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "generate"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

- - +

2 posts tagged with "generate"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/github.html b/blog/tags/github.html index 1d18cf95aa..ec08065b07 100644 --- a/blog/tags/github.html +++ b/blog/tags/github.html @@ -19,13 +19,13 @@ - - + +
-

13 posts tagged with "github"

View All Tags
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more
By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

- - +

13 posts tagged with "github"

View All Tags
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more
By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/gitpod.html b/blog/tags/gitpod.html index ba4b1fe0e6..01b5221a8b 100644 --- a/blog/tags/gitpod.html +++ b/blog/tags/gitpod.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "gitpod"

View All Tags
By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more
- - +

One post tagged with "gitpod"

View All Tags
By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more
+ + \ No newline at end of file diff --git a/blog/tags/gpt.html b/blog/tags/gpt.html index 512b357c67..c1e63639eb 100644 --- a/blog/tags/gpt.html +++ b/blog/tags/gpt.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "gpt"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

7 posts tagged with "gpt"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hack.html b/blog/tags/hack.html index e874ae35d3..182e4c2e2a 100644 --- a/blog/tags/hack.html +++ b/blog/tags/hack.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "hack"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "hack"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/hackathon.html b/blog/tags/hackathon.html index b9cd51088e..2a2a9c913b 100644 --- a/blog/tags/hackathon.html +++ b/blog/tags/hackathon.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "hackathon"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

- - +

3 posts tagged with "hackathon"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hacktoberfest.html b/blog/tags/hacktoberfest.html index 467d65b632..7f9b25108f 100644 --- a/blog/tags/hacktoberfest.html +++ b/blog/tags/hacktoberfest.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "hacktoberfest"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

- - +

2 posts tagged with "hacktoberfest"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/haskell.html b/blog/tags/haskell.html index cfb83457e8..b96e76b4f3 100644 --- a/blog/tags/haskell.html +++ b/blog/tags/haskell.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "haskell"

View All Tags
By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more
By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

- - +

3 posts tagged with "haskell"

View All Tags
By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more
By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hiring.html b/blog/tags/hiring.html index c1b668ee66..82f8576a7c 100644 --- a/blog/tags/hiring.html +++ b/blog/tags/hiring.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "hiring"

View All Tags
By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more
- - +

One post tagged with "hiring"

View All Tags
By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more
+ + \ No newline at end of file diff --git a/blog/tags/indie-hacker.html b/blog/tags/indie-hacker.html index 558c8920fd..18b884ddac 100644 --- a/blog/tags/indie-hacker.html +++ b/blog/tags/indie-hacker.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "IndieHacker"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "IndieHacker"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/interview.html b/blog/tags/interview.html index d6ba3fcc61..27b2bd771a 100644 --- a/blog/tags/interview.html +++ b/blog/tags/interview.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Interview"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "Interview"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/javascript.html b/blog/tags/javascript.html index 573ac2d740..18dddf911e 100644 --- a/blog/tags/javascript.html +++ b/blog/tags/javascript.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "javascript"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

- - +

2 posts tagged with "javascript"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/job.html b/blog/tags/job.html index 0c62806e64..4c52e95d61 100644 --- a/blog/tags/job.html +++ b/blog/tags/job.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "job"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
- - +

One post tagged with "job"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
+ + \ No newline at end of file diff --git a/blog/tags/jobs.html b/blog/tags/jobs.html index 2bcf346ca8..36fd3f5f2e 100644 --- a/blog/tags/jobs.html +++ b/blog/tags/jobs.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "jobs"

View All Tags
By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more
- - +

One post tagged with "jobs"

View All Tags
By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more
+ + \ No newline at end of file diff --git a/blog/tags/junior-developers.html b/blog/tags/junior-developers.html index d50820232e..e176fb918e 100644 --- a/blog/tags/junior-developers.html +++ b/blog/tags/junior-developers.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Junior Developers"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Junior Developers"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/langchain.html b/blog/tags/langchain.html index 7d8ae3cbe4..cf6bac22fe 100644 --- a/blog/tags/langchain.html +++ b/blog/tags/langchain.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "langchain"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

2 posts tagged with "langchain"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/language.html b/blog/tags/language.html index 4a608e56a0..c0223c10e3 100644 --- a/blog/tags/language.html +++ b/blog/tags/language.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "language"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

- - +

5 posts tagged with "language"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/laravel.html b/blog/tags/laravel.html index 64c2fa8743..9f461a52b6 100644 --- a/blog/tags/laravel.html +++ b/blog/tags/laravel.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "laravel"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
- - +

One post tagged with "laravel"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
+ + \ No newline at end of file diff --git a/blog/tags/launch-week.html b/blog/tags/launch-week.html index 968ed2eafb..9461d23bd0 100644 --- a/blog/tags/launch-week.html +++ b/blog/tags/launch-week.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "launch-week"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more
By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

- - +

7 posts tagged with "launch-week"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more
By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/mage.html b/blog/tags/mage.html index ed4b7f471e..e06fab3861 100644 --- a/blog/tags/mage.html +++ b/blog/tags/mage.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "mage"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

- - +

2 posts tagged with "mage"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/marketing.html b/blog/tags/marketing.html index 5c37c566aa..7c3a4ca05b 100644 --- a/blog/tags/marketing.html +++ b/blog/tags/marketing.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "marketing"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
- - +

One post tagged with "marketing"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
+ + \ No newline at end of file diff --git a/blog/tags/meme.html b/blog/tags/meme.html index 6346eacf35..fd54fd9b4c 100644 --- a/blog/tags/meme.html +++ b/blog/tags/meme.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "meme"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
- - +

One post tagged with "meme"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
+ + \ No newline at end of file diff --git a/blog/tags/ml.html b/blog/tags/ml.html index 374fad2073..7883ea4760 100644 --- a/blog/tags/ml.html +++ b/blog/tags/ml.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "ML"

View All Tags
By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more
- - +

One post tagged with "ML"

View All Tags
By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more
+ + \ No newline at end of file diff --git a/blog/tags/new-hire.html b/blog/tags/new-hire.html index a1b8aeed83..7b334820c8 100644 --- a/blog/tags/new-hire.html +++ b/blog/tags/new-hire.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "new-hire"

View All Tags
By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more
By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

- - +

2 posts tagged with "new-hire"

View All Tags
By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more
By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/nextjs.html b/blog/tags/nextjs.html new file mode 100644 index 0000000000..d9a8059a62 --- /dev/null +++ b/blog/tags/nextjs.html @@ -0,0 +1,31 @@ + + + + + +One post tagged with "nextjs" | Wasp + + + + + + + + + + + + + + + + + + + +
+

One post tagged with "nextjs"

View All Tags
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
+ + + + \ No newline at end of file diff --git a/blog/tags/node.html b/blog/tags/node.html index ecf8299450..149afbdf22 100644 --- a/blog/tags/node.html +++ b/blog/tags/node.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "node"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

3 posts tagged with "node"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/nodejs.html b/blog/tags/nodejs.html index a13c4f0386..ed79bcfb4b 100644 --- a/blog/tags/nodejs.html +++ b/blog/tags/nodejs.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "nodejs"

View All Tags
By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

- - +

3 posts tagged with "nodejs"

View All Tags
By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/open-source.html b/blog/tags/open-source.html index 7e9a13951b..e3ec304124 100644 --- a/blog/tags/open-source.html +++ b/blog/tags/open-source.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "open-source"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
- - +

One post tagged with "open-source"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
+ + \ No newline at end of file diff --git a/blog/tags/openai.html b/blog/tags/openai.html index e71b72452a..343685879e 100644 --- a/blog/tags/openai.html +++ b/blog/tags/openai.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "openai"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

- - +

2 posts tagged with "openai"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/optimistic.html b/blog/tags/optimistic.html index 586f7d9279..a194507435 100644 --- a/blog/tags/optimistic.html +++ b/blog/tags/optimistic.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "optimistic"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
- - +

One post tagged with "optimistic"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
+ + \ No newline at end of file diff --git a/blog/tags/pern.html b/blog/tags/pern.html index acc74219ac..cf30ea7f99 100644 --- a/blog/tags/pern.html +++ b/blog/tags/pern.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "PERN"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "PERN"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/prd.html b/blog/tags/prd.html index 761dd4a416..0e90ab19c1 100644 --- a/blog/tags/prd.html +++ b/blog/tags/prd.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "prd"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "prd"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/prisma.html b/blog/tags/prisma.html index 6014655560..483da0eb7a 100644 --- a/blog/tags/prisma.html +++ b/blog/tags/prisma.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "prisma"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

- - +

2 posts tagged with "prisma"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/product-requirement.html b/blog/tags/product-requirement.html index 57232a359e..df173a6b54 100644 --- a/blog/tags/product-requirement.html +++ b/blog/tags/product-requirement.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "product requirement"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "product requirement"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/product-update.html b/blog/tags/product-update.html index fb2f180087..33b0b0f5bb 100644 --- a/blog/tags/product-update.html +++ b/blog/tags/product-update.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "product-update"

View All Tags
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more
By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

- - +

2 posts tagged with "product-update"

View All Tags
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more
By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/programming.html b/blog/tags/programming.html index b8f853f11c..5e3367ff65 100644 --- a/blog/tags/programming.html +++ b/blog/tags/programming.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "programming"

View All Tags
By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

- - +

2 posts tagged with "programming"

View All Tags
By Matija Sosic
14 min read

On the Importance of RFCs in Programming

Read more
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/rails.html b/blog/tags/rails.html index af79cb531e..0d74d66ad5 100644 --- a/blog/tags/rails.html +++ b/blog/tags/rails.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "rails"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
- - +

One post tagged with "rails"

View All Tags
By Vinny
12 min read

Why We Don't Have a Laravel For JavaScript... Yet

Read more
+ + \ No newline at end of file diff --git a/blog/tags/react.html b/blog/tags/react.html index af79e45824..ce76e810a2 100644 --- a/blog/tags/react.html +++ b/blog/tags/react.html @@ -3,7 +3,7 @@ -10 posts tagged with "react" | Wasp +11 posts tagged with "react" | Wasp @@ -19,13 +19,13 @@ - - + +
-

10 posts tagged with "react"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more →

By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

- - +

11 posts tagged with "react"

View All Tags
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more →

By Vinny
9 min read

The first framework that lets you visualize your React/NodeJS app's code

Read more →

By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/real-time.html b/blog/tags/real-time.html index dca6df0068..868e1843b6 100644 --- a/blog/tags/real-time.html +++ b/blog/tags/real-time.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "real-time"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "real-time"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/blog/tags/reddit.html b/blog/tags/reddit.html index 661ce21aff..f863bec3cd 100644 --- a/blog/tags/reddit.html +++ b/blog/tags/reddit.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Reddit"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Reddit"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/saa-s.html b/blog/tags/saa-s.html index 951420aa20..49cfb8a986 100644 --- a/blog/tags/saa-s.html +++ b/blog/tags/saa-s.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "SaaS"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "SaaS"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/saas.html b/blog/tags/saas.html index f4902c15f3..a346fecaf6 100644 --- a/blog/tags/saas.html +++ b/blog/tags/saas.html @@ -19,13 +19,13 @@ - - + +
-

4 posts tagged with "saas"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

4 posts tagged with "saas"

View All Tags
By Vinny
7 min read

Building and Selling a GPT Wrapper SaaS in 5 Months

Read more
By Vinny
10 min read

Open SaaS: our free, open-source SaaS starter

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/showcase.html b/blog/tags/showcase.html index 754858c658..8a862f8d85 100644 --- a/blog/tags/showcase.html +++ b/blog/tags/showcase.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "showcase"

View All Tags
By Matija Sosic
4 min read

What can you build with Wasp?

Read more
- - +

One post tagged with "showcase"

View All Tags
By Matija Sosic
4 min read

What can you build with Wasp?

Read more
+ + \ No newline at end of file diff --git a/blog/tags/solopreneur.html b/blog/tags/solopreneur.html index dcd4c52b4e..5161008287 100644 --- a/blog/tags/solopreneur.html +++ b/blog/tags/solopreneur.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Solopreneur"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "Solopreneur"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/startup.html b/blog/tags/startup.html index 2293e1d506..428248a3a2 100644 --- a/blog/tags/startup.html +++ b/blog/tags/startup.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "startup"

View All Tags
By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more
By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

- - +

3 posts tagged with "startup"

View All Tags
By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more
By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/startups.html b/blog/tags/startups.html index 78521f931c..12131da88c 100644 --- a/blog/tags/startups.html +++ b/blog/tags/startups.html @@ -19,13 +19,13 @@ - - + +
-

15 posts tagged with "startups"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

- - +

15 posts tagged with "startups"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/state-of-js.html b/blog/tags/state-of-js.html index f2b80fbe06..38ef9ab450 100644 --- a/blog/tags/state-of-js.html +++ b/blog/tags/state-of-js.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "StateOfJS"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
- - +

One post tagged with "StateOfJS"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
+ + \ No newline at end of file diff --git a/blog/tags/stripe.html b/blog/tags/stripe.html index 1aee3beced..a32bfc1e26 100644 --- a/blog/tags/stripe.html +++ b/blog/tags/stripe.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "stripe"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "stripe"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/supabase.html b/blog/tags/supabase.html index b56094eaa9..e1f6d84326 100644 --- a/blog/tags/supabase.html +++ b/blog/tags/supabase.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Supabase"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
- - +

One post tagged with "Supabase"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
+ + \ No newline at end of file diff --git a/blog/tags/tech-career.html b/blog/tags/tech-career.html index 3e8370ba10..16cc794406 100644 --- a/blog/tags/tech-career.html +++ b/blog/tags/tech-career.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Tech Career"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Tech Career"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/tech.html b/blog/tags/tech.html index ceb9df74e1..7690d81f47 100644 --- a/blog/tags/tech.html +++ b/blog/tags/tech.html @@ -3,7 +3,7 @@ -One post tagged with "tech" | Wasp +2 posts tagged with "tech" | Wasp @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "tech"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
- - +

2 posts tagged with "tech"

View All Tags
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/tutorial.html b/blog/tags/tutorial.html index 0f7b3151fd..1edc1bd6fc 100644 --- a/blog/tags/tutorial.html +++ b/blog/tags/tutorial.html @@ -3,7 +3,7 @@ -2 posts tagged with "tutorial" | Wasp +3 posts tagged with "tutorial" | Wasp @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "tutorial"

View All Tags
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more
By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

- - +

3 posts tagged with "tutorial"

View All Tags
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/typescript.html b/blog/tags/typescript.html index 1f6bcd762c..a2f4bf370a 100644 --- a/blog/tags/typescript.html +++ b/blog/tags/typescript.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "typescript"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

- - +

2 posts tagged with "typescript"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/update.html b/blog/tags/update.html index c7f3273f7f..571bc68d00 100644 --- a/blog/tags/update.html +++ b/blog/tags/update.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "update"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more
By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

- - +

5 posts tagged with "update"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #6: The Fun Side of Web Development 🕺 🪩

Read more
By Matija Sosic
5 min read

Wasp Launch Week #5: Waspnado 🐝 🌪️

Read more →

By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/updates.html b/blog/tags/updates.html index 633a56fd5e..aedc061bdd 100644 --- a/blog/tags/updates.html +++ b/blog/tags/updates.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "updates"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
- - +

One post tagged with "updates"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
+ + \ No newline at end of file diff --git a/blog/tags/wasp-ai.html b/blog/tags/wasp-ai.html index 02571a05e7..c6ee57889e 100644 --- a/blog/tags/wasp-ai.html +++ b/blog/tags/wasp-ai.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "wasp-ai"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

- - +

2 posts tagged with "wasp-ai"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/wasp.html b/blog/tags/wasp.html index 323dbcf1b2..520a528f6d 100644 --- a/blog/tags/wasp.html +++ b/blog/tags/wasp.html @@ -19,13 +19,13 @@ - - + +
-

42 posts tagged with "wasp"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

- - +

42 posts tagged with "wasp"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/web-dev.html b/blog/tags/web-dev.html index 8d3f98e94e..1fdc536fbb 100644 --- a/blog/tags/web-dev.html +++ b/blog/tags/web-dev.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "WebDev"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

3 posts tagged with "WebDev"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/web-development.html b/blog/tags/web-development.html index 73d30b673c..d009552217 100644 --- a/blog/tags/web-development.html +++ b/blog/tags/web-development.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "web-development"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
- - +

One post tagged with "web-development"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
+ + \ No newline at end of file diff --git a/blog/tags/webdev.html b/blog/tags/webdev.html index 694e3e1062..4c6a11d79d 100644 --- a/blog/tags/webdev.html +++ b/blog/tags/webdev.html @@ -3,7 +3,7 @@ -31 posts tagged with "webdev" | Wasp +32 posts tagged with "webdev" | Wasp @@ -19,13 +19,13 @@ - - + +
-

31 posts tagged with "webdev"

View All Tags
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more
By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

- - +

32 posts tagged with "webdev"

View All Tags
By Lucas Lima do Nascimento
16 min read

How to Add Auth with Lucia to Your React/Next.js App - A Step by Step Guide

Read more
By Vinny
12 min read

How to get a Web Dev Job in 2024

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/websockets.html b/blog/tags/websockets.html index baa7e20747..f37d7f02d4 100644 --- a/blog/tags/websockets.html +++ b/blog/tags/websockets.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "websockets"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "websockets"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/blog/tags/windows.html b/blog/tags/windows.html index 269433d6a0..6c5426407d 100644 --- a/blog/tags/windows.html +++ b/blog/tags/windows.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "windows"

View All Tags
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more
- - +

One post tagged with "windows"

View All Tags
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more
+ + \ No newline at end of file diff --git a/blog/tags/wsl.html b/blog/tags/wsl.html index 17e264fa22..b5819b7118 100644 --- a/blog/tags/wsl.html +++ b/blog/tags/wsl.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "wsl"

View All Tags
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more
- - +

One post tagged with "wsl"

View All Tags
By Boris Martinović
10 min read

A Guide to Windows Development with Wasp & WSL

Read more
+ + \ No newline at end of file diff --git a/docs.html b/docs.html index d18da994aa..0909c59876 100644 --- a/docs.html +++ b/docs.html @@ -19,8 +19,8 @@ - - + +
@@ -29,7 +29,7 @@ which are in their essence Node.js functions that execute on the server and can, thanks to Wasp, very easily be called from the client.

First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up":

main.wasp
// Queries have automatic cache invalidation and are type-safe.
query getRecipes {
fn: import { getRecipes } from "@src/recipe/operations",
entities: [Recipe],
}

// Actions are type-safe and can be used to perform side-effects.
action addRecipe {
fn: import { addRecipe } from "@src/recipe/operations",
entities: [Recipe],
}

... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):

src/recipe/operations.ts
// Wasp generates the types for you.
import { type GetRecipes } from "wasp/server/operations";
import { type Recipe } from "wasp/entities";

export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
return context.entities.Recipe.findMany( // Prisma query
{ where: { user: { id: context.user.id } } }
);
};

export const addRecipe ...

Now we can very easily use these in our React components!

For the end, let's create a home page of our app.

First, we define it in main.wasp:

main.wasp
...

route HomeRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@src/pages/HomePage",
authRequired: true // Will send user to /login if not authenticated.
}

and then implement it as a React component in JS/TS (that calls the Operations we previously defined):

src/pages/HomePage.tsx
import { useQuery, getRecipes } from "wasp/client/operations";
import { type User } from "wasp/entities";

export function HomePage({ user }: { user: User }) {
// Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here!

if (isLoading) {
return <div>Loading...</div>;
}

return (
<div>
<h1>Recipes</h1>
<ul>
{recipes ? recipes.map((recipe) => (
<li key={recipe.id}>
<div>{recipe.title}</div>
<div>{recipe.description}</div>
</li>
)) : 'No recipes defined yet!'}
</ul>
</div>
);
}

And voila! We are listing all the recipes in our app 🎉

This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the Todo app tutorial.

note

Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.

When to use Wasp

Wasp addresses the same core problems that typical web app frameworks are addressing, and it in big part looks, swims and quacks like a web app framework.

Best used for

  • building full-stack web apps (like e.g. Airbnb or Asana)
  • quickly starting a web app with industry best practices
  • to be used alongside modern web dev stack (React and Node.js are currently supported)

Avoid using Wasp for

  • building static/presentational websites
  • to be used as a no-code solution
  • to be a solve-it-all tool in a single language

Wasp is a DSL

note

You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below.

Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a simple programming language that understands your code and can do a lot of things for you.

Wasp is a programming language, but a specific kind: it is specialized for a single purpose: building modern web applications. We call such languages DSLs (Domain Specific Language).

Other examples of DSLs that are often used today are e.g. SQL for databases and HTML for web page layouts. The main advantage and reason why DSLs exist is that they need to do only one task (e.g. database queries) so they can do it well and provide the best possible experience for the developer.

The same idea stands behind Wasp - a language that will allow developers to build modern web applications with 10x less code and less stack-specific knowledge.

- - + + \ No newline at end of file diff --git a/docs/0.11.8.html b/docs/0.11.8.html index 9b74fd8ec4..a1d73acbc1 100644 --- a/docs/0.11.8.html +++ b/docs/0.11.8.html @@ -19,8 +19,8 @@ - - + +
@@ -30,7 +30,7 @@ which are in their essence a Node.js functions that execute on server and can, thanks to Wasp, very easily be called from the client.

First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up":

main.wasp
// Queries have automatic cache invalidation and are type-safe.
query getRecipes {
fn: import { getRecipes } from "@server/recipe.js",
entities: [Recipe],
}

// Actions are type-safe and can be used to perform side-effects.
action addRecipe {
fn: import { addRecipe } from "@server/recipe.js",
entities: [Recipe],
}

... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):

src/server/recipe.ts
// Wasp generates types for you.
import type { GetRecipes } from "@wasp/queries/types";
import type { Recipe } from "@wasp/entities";

export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
return context.entities.Recipe.findMany( // Prisma query
{ where: { user: { id: context.user.id } } }
);
};

export const addRecipe ...

Now we can very easily use these in our React components!

For the end, let's create a home page of our app.

First we define it in main.wasp:

main.wasp
...

route HomeRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/HomePage",
authRequired: true // Will send user to /login if not authenticated.
}

and then implement it as a React component in JS/TS (that calls the Operations we previously defined):

src/client/pages/HomePage.tsx
import getRecipes from "@wasp/queries/getRecipes";
import { useQuery } from "@wasp/queries";
import type { User } from "@wasp/entities";

export function HomePage({ user }: { user: User }) {
// Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here!

if (isLoading) {
return <div>Loading...</div>;
}

return (
<div>
<h1>Recipes</h1>
<ul>
{recipes ? recipes.map((recipe) => (
<li key={recipe.id}>
<div>{recipe.title}</div>
<div>{recipe.description}</div>
</li>
)) : 'No recipes defined yet!'}
</ul>
</div>
);
}

And voila! We are listing all the recipes in our app 🎉

This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the Todo app tutorial.

note

Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.

When to use Wasp

Wasp is addressing the same core problems that typical web app frameworks are addressing, and it in big part looks, swims and quacks like a web app framework.

Best used for

  • building full-stack web apps (like e.g. Airbnb or Asana)
  • quickly starting a web app with industry best practices
  • to be used alongside modern web dev stack (currently supported React and Node)

Avoid using Wasp for

  • building static/presentational websites
  • to be used as a no-code solution
  • to be a solve-it-all tool in a single language

Wasp is a DSL

note

You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below.

Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a programming language that understands your code and can do a lot of things for you.

Wasp is a programming language, but a specific kind: it is specialized for a single purpose: building modern web applications. We call such languages DSLs (Domain Specific Language).

Other examples of DSLs that are often used today are e.g. SQL for databases and HTML for web page layouts. The main advantage and reason why DSLs exist is that they need to do only one task (e.g. database queries) so they can do it well and provide the best possible experience for the developer.

The same idea stands behind Wasp - a language that will allow developers to build modern web applications with 10x less code and less stack-specific knowledge.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/apis.html b/docs/0.11.8/advanced/apis.html index 8039e58fb6..699b683e26 100644 --- a/docs/0.11.8/advanced/apis.html +++ b/docs/0.11.8/advanced/apis.html @@ -19,14 +19,14 @@ - - + +
Version: 0.11.8

Custom HTTP API Endpoints

In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

How to Create an API

APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

To create a Wasp API, you must:

  1. Declare the API in Wasp using the api declaration
  2. Define the API's NodeJS implementation

After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

Declaring the API in Wasp

First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

main.wasp
// ...

api fooBar { // APIs and their implementations don't need to (but can) have the same name.
fn: import { fooBar } from "@server/apis.js",
httpRoute: (GET, "/foo/bar")
}

Read more about the supported fields in the API Reference.

Defining the API's NodeJS Implementation

After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

  1. req: Express Request object
  2. res: Express Response object
  3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
src/server/apis.js
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
};

Using the API

Using the API externally

To use the API externally, you simply call the endpoint using the method and path you used.

For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

Using the API from the Client

To use the API from your client, including with auth support, you can import the Axios wrapper from @wasp/api and invoke a call. For example:

src/client/pages/SomePage.jsx
import React, { useEffect } from "react";
import api from "@wasp/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}

export const Foo = () => {
useEffect(() => {
fetchCustomRoute();
}, []);

return <>// ...</>;
};

Making Sure CORS Works

APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

You can do this by defining custom middleware for your APIs in the Wasp file.

For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

main.wasp
apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo"
}

And then in the implementation file:

src/server/apis.js
export const apiMiddleware = (config) => {
return config;
};

We are returning the default middleware which enables CORS for all APIs under the /foo path.

For more information about middleware configuration, please see: Middleware Configuration

Using Entities in APIs

In many cases, resources used in APIs will be Entities. To use an Entity in your API, add it to the api declaration in Wasp:

main.wasp
api fooBar {
fn: import { fooBar } from "@server/apis.js",
entities: [Task],
httpRoute: (GET, "/foo/bar")
}

Wasp will inject the specified Entity into the APIs context argument, giving you access to the Entity's Prisma API:

src/server/apis.js
export const fooBar = (req, res, context) => {
res.json({ count: await context.entities.Task.count() });
};

The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

API Reference

main.wasp
api fooBar {
fn: import { fooBar } from "@server/apis.js",
httpRoute: (GET, "/foo/bar"),
entities: [Task],
auth: true,
middlewareConfigFn: import { apiMiddleware } from "@server/apis.js"
}

The api declaration has the following fields:

  • fn: ServerImport required

    The import statement of the APIs NodeJs implementation.

  • httpRoute: (HttpMethod, string) required

    The HTTP (method, path) pair, where the method can be one of:

    • ALL, GET, POST, PUT or DELETE
    • and path is an Express path string.
  • entities: [Entity]

    A list of entities you wish to use inside your API. You can read more about it here.

  • auth: bool

    If auth is enabled, this will default to true and provide a context.user object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to false.

  • middlewareConfigFn: ServerImport

    The import statement to an Express middleware config function for this API. See more in middleware section of the docs.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/deployment/cli.html b/docs/0.11.8/advanced/deployment/cli.html index 871b5668b5..beab2b9320 100644 --- a/docs/0.11.8/advanced/deployment/cli.html +++ b/docs/0.11.8/advanced/deployment/cli.html @@ -19,15 +19,15 @@ - - + +
Version: 0.11.8

Deploying with the Wasp CLI

Wasp CLI can deploy your full-stack application with only a single command. The command automates the manual deployment process and is the recommended way of deploying Wasp apps.

Supported Providers

Wasp supports automated deployment to the following providers:

  • Fly.io - they offer 5$ free credit each month
  • Railway (coming soon, track it here #1157)

Fly.io

Prerequisites

Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.

You can add the required credit card information on the account's billing page.

Fly.io CLI

You will need the flyctl CLI installed on your machine before you can deploy to Fly.io.

Deploying

Using the Wasp CLI, you can easily deploy a new app to Fly.io with just a single command:

wasp deploy fly launch my-wasp-app mia

Please do not CTRL-C or exit your terminal while the commands are running.

Under the covers, this runs the equivalent of the following commands:

wasp deploy fly setup my-wasp-app mia
wasp deploy fly create-db mia
wasp deploy fly deploy

The commands above use the app basename my-wasp-app and deploy it to the Miami, Florida (US) region (called mia). Read more about Fly.io regions here.

Unique Name

Your app name must be unique across all of Fly or deployment will fail.

The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:

  • my-wasp-app-client
  • my-wasp-app-server
  • my-wasp-app-db

You'll notice that Wasp creates two new files in your project root directory:

  • fly-server.toml
  • fly-client.toml

You should include these files in your version control so that you can deploy your app with a single command in the future.

Using a Custom Domain For Your App

Setting up a custom domain is a three-step process:

  1. You need to add your domain to your Fly client app. You can do this by running:
wasp deploy fly cmd --context client certs create mycoolapp.com
Use Your Domain

Make sure to replace mycoolapp.com with your domain in all of the commands mentioned in this section.

This command will output the instructions to add the DNS records to your domain. It will look something like this:

You can direct traffic to mycoolapp.com by:

1: Adding an A record to your DNS service which reads

A @ 66.241.1XX.154

You can validate your ownership of mycoolapp.com by:

2: Adding an AAAA record to your DNS service which reads:

AAAA @ 2a09:82XX:1::1:ff40
  1. You need to add the DNS records for your domain:

    This will depend on your domain provider, but it should be a matter of adding an A record for @ and an AAAA record for @ with the values provided by the previous command.

  2. You need to set your domain as the WASP_WEB_CLIENT_URL environment variable for your server app:

wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com

We need to do this to keep our CORS configuration up to date.

That's it, your app should be available at https://mycoolapp.com! 🎉

API Reference

launch

launch is a convenience command that runs setup, create-db, and deploy in sequence.

wasp deploy fly launch <app-name> <region>

It accepts the following arguments:

  • <app-name> - the name of your app required

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

It gives you the same result as running the following commands:

wasp deploy fly setup <app-name> <region>
wasp deploy fly create-db <region>
wasp deploy fly deploy

Environment Variables

If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the --server-secret option:

wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>

setup

setup will create your client and server apps on Fly, and add some secrets, but does not deploy them.

wasp deploy fly setup <app-name> <region>

It accepts the following arguments:

  • <app-name> - the name of your app required

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

After running setup, Wasp creates two new files in your project root directory: fly-server.toml and fly-client.toml. You should include these files in your version control.

You can edit the fly-server.toml and fly-client.toml files to further configure your Fly deployments. Wasp will use the TOML files when you run deploy.

If you want to maintain multiple apps, you can add the --fly-toml-dir <abs-path> option to point to different directories, like "dev" or "staging".

Execute Only Once

You should only run setup once per app. If you run it multiple times, it will create unnecessary apps on Fly.

create-db

create-db will create a new database for your app.

wasp deploy fly create-db <region>

It accepts the following arguments:

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

Execute Only Once

You should only run create-db once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.

deploy

wasp deploy fly deploy

deploy pushes your client and server live.

Run this command whenever you want to update your deployed app with the latest changes:

wasp deploy fly deploy

cmd

If want to run arbitrary Fly commands (e.g. flyctl secrets list for your server app), here's how to do it:

wasp deploy fly cmd secrets list --context server

Fly.io Regions

Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.

Read more on Fly regions here.

You can find the list of all available Fly regions by running:

flyctl platform regions

Environment Variables

If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the secrets set command:

wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server

Mutliple Fly Organizations

If you have multiple organizations, you can specify a --org option. For example:

wasp deploy fly launch my-wasp-app mia --org hive

Building Locally

Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.

If you want to build locally, supply the --build-locally option to wasp deploy fly launch or wasp deploy fly deploy.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/deployment/manually.html b/docs/0.11.8/advanced/deployment/manually.html index 0d308d0007..e06964f5a3 100644 --- a/docs/0.11.8/advanced/deployment/manually.html +++ b/docs/0.11.8/advanced/deployment/manually.html @@ -19,8 +19,8 @@ - - + +
@@ -31,7 +31,7 @@ Set it to a random string at least 32 characters long (you can use an online generator).

Using an external auth method?

If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

3. Deploying the Web Client (frontend)

To build the web app, position yourself in .wasp/build/web-app directory:

cd .wasp/build/web-app

Run

npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

The command above will build the web client and put it in the build/ directory in the web-app directory.

Since the app's frontend is just a bunch of static files, you can deploy it to any static hosting provider.

4. Deploying the Database

Any PostgreSQL database will do, as long as you set the DATABASE_URL env var correctly and ensure that the database is accessible from the server.

Different Providers

We'll cover a few different deployment providers below:

  • Fly.io (server and database)
  • Netlify (client)
  • Railway (server, client and database)
  • Heroku (server and database)

Fly.io

We automated this process for you

If you want to do all of the work below with one command, you can use the Wasp CLI.

Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command.

Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the flyctl CLI.

note

Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.

Additionally, fly is a symlink for flyctl on most systems and they can be used interchangeably.

Make sure you are logged in with flyctl CLI. You can check if you are logged in with flyctl auth whoami, and if you are not, you can log in with flyctl auth login.

Set Up a Fly.io App

info

You need to do this only once per Wasp app.

Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.

After you have built the app, position yourself in .wasp/build/ directory:

cd .wasp/build

Next, run the launch command to set up a new app and create a fly.toml file:

flyctl launch --remote-only

This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.

  • Say yes to Would you like to set up a Postgresql database now? and select Development. Fly.io will set a DATABASE_URL for you.

  • Say no to Would you like to deploy now? (and to any additional questions).

    We still need to set up several environment variables.

What if the database setup fails?

If your attempts to initiate a new app fail for whatever reason, then you should run flyctl apps destroy <app-name> before trying again. Fly does not allow you to create multiple apps with the same name.

What does it look like when your DB is deployed correctly?

When your DB is deployed correctly, you'll see it in the Fly.io dashboard:

image

Next, let's copy the fly.toml file up to our Wasp project dir for safekeeping.

cp fly.toml ../../

Next, let's add a few more environment variables:

flyctl secrets set PORT=8080
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
note

If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

Using an external auth method?

If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

If you want to make sure you've added your secrets correctly, run flyctl secrets list in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.

Deploy to a Fly.io App

While still in the .wasp/build/ directory, run:

flyctl deploy --remote-only --config ../../fly.toml

This will build and deploy the backend of your Wasp app on Fly.io to https://<app-name>.fly.dev 🤘🎸

Now, if you haven't, you can deploy your frontend and add the client url by running flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_frontend>. We suggest using Netlify for your frontend, but you can use any static hosting provider.

Additionally, some useful flyctl commands:

flyctl logs
flyctl secrets list
flyctl ssh console

Redeploying After Wasp Builds

When you rebuild your Wasp app (with wasp build), it will remove your .wasp/build/ directory. In there, you may have a fly.toml from any prior Fly.io deployments.

While we will improve this process in the future, in the meantime, you have a few options:

  1. Copy the fly.toml file to a versioned directory, like your Wasp project dir.

    From there, you can reference it in flyctl deploy --config <path> commands, like above.

  2. Backup the fly.toml file somewhere before running wasp build, and copy it into .wasp/build/ after.

    When the fly.toml file exists in .wasp/build/ dir, you do not need to specify the --config <path>.

  3. Run flyctl config save -a <app-name> to regenerate the fly.toml file from the remote state stored in Fly.io.

Netlify

Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and Netlify CLI installed to follow these instructions.

Make sure you are logged in with Netlify CLI. You can check if you are logged in with netlify status, and if you are not, you can log in with netlify login.

First, make sure you have built the Wasp app. We'll build the client web app next.

To build the web app, position yourself in .wasp/build/web-app directory:

cd .wasp/build/web-app

Run

npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

We can now deploy the client with:

netlify deploy

Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.

The final step is to run:

netlify deploy --prod

That is it! Your client should be live at https://<app-name>.netlify.app

note

Make sure you set this URL as the WASP_WEB_CLIENT_URL environment variable in your server hosting environment (e.g., Fly.io or Heroku).

Railway

Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their plans page for more info.

Prerequisites

To get started, follow these steps:

  1. Make sure your Wasp app is built by running wasp build in the project dir.

  2. Create a Railway account

    Free Tier

    Sign up with your GitHub account to be eligible for the free tier

  3. Install the Railway CLI

  4. Run railway login and a browser tab will open to authenticate you.

Create New Project

Let's create our Railway project:

  1. Go to your Railway dashboard, click on New Project, and select Provision PostgreSQL from the dropdown menu.
  2. Once it initializes, right-click on the New button in the top right corner and select Empty Service.
  3. Once it initializes, click on it, go to Settings > General and change the name to server
  4. Go ahead and create another empty service and name it client

Changing the name

Deploy Your App to Railway

Setup Domains

We'll need the domains for both the server and client services:

  1. Go to the server instance's Settings tab, and click Generate Domain.
  2. Do the same under the client's Settings.

Copy the domains as we will need them later.

Deploying the Server

Let's deploy our server first:

  1. Move into your app's .wasp/build/ directory:

    cd .wasp/build
  2. Link your app build to your newly created Railway project:

    railway link
  3. Go into the Railway dashboard and set up the required env variables:

    Open the Settings and go to the Variables tab:

    • click Variable reference and select DATABASE_URL (it will populate it with the correct value)

    • add WASP_WEB_CLIENT_URL - enter the the client domain (e.g. https://client-production-XXXX.up.railway.app)

    • add JWT_SECRET - enter a random string at least 32 characters long (use an online generator)

      Using an external auth method?

      If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

  4. Push and deploy the project:

railway up

Select server when prompted with Select Service.

Railway will now locate the Dockerfile and deploy your server 👍

Deploying the Client

  1. Next, change into your app's frontend build directory .wasp/build/web-app:

    cd web-app
  2. Create the production build, using the server domain as the REACT_APP_API_URL:

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
  3. Next, we want to link this specific frontend directory to our project as well:

    railway link
  4. We need to configure Railway's static hosting for our client.

    Setting Up Static Hosting

    Copy the build folder within the web-app directory to dist:

    cp -r build dist

    We'll need to create the following files:

    • Dockerfile with:

      Dockerfile
      FROM pierrezemb/gostatic
      CMD [ "-fallback", "index.html" ]
      COPY ./dist/ /srv/http/
    • .dockerignore with:

      .dockerignore
      node_modules/

    You'll need to repeat these steps each time you run wasp build as it will remove the .wasp/build/web-app directory.

    Here's a useful shell script to do the process

    If you want to automate the process, save the following as deploy_client.sh in the root of your project:

    deploy_client.sh
    #!/usr/bin/env bash

    if [ -z "$REACT_APP_API_URL" ]
    then
    echo "REACT_APP_API_URL is not set"
    exit 1
    fi

    wasp build
    cd .wasp/build/web-app

    npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build

    cp -r build dist

    dockerfile_contents=$(cat <<EOF
    FROM pierrezemb/gostatic
    CMD [ "-fallback", "index.html" ]
    COPY ./dist/ /srv/http/
    EOF
    )

    dockerignore_contents=$(cat <<EOF
    node_modules/
    EOF
    )

    echo "$dockerfile_contents" > Dockerfile
    echo "$dockerignore_contents" > .dockerignore

    railway up

    Make it executable with:

    chmod +x deploy_client.sh

    You can run it with:

    REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
  5. Set the PORT environment variable to 8043 under the Variables tab.

  6. Deploy the client and select client when prompted with Select Service:

railway up

Conclusion

And now your Wasp should be deployed! 🐝 🚂 🚀

Back in your Railway dashboard, click on your project and you should see your newly deployed services: Postgres, Server, and Client.

Updates & Redeploying

When you make updates and need to redeploy:

  • run wasp build to rebuild your app
  • run railway up in the .wasp/build directory (server)
  • repeat all the steps in the .wasp/build/web-app directory (client)

Heroku

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we recommend using an alternative provider like Fly.io for your first apps.

You will need Heroku account, heroku CLI and docker CLI installed to follow these instructions.

Make sure you are logged in with heroku CLI. You can check if you are logged in with heroku whoami, and if you are not, you can log in with heroku login.

Set Up a Heroku App

info

You need to do this only once per Wasp app.

Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:

heroku create <app-name>

Unless you have an external Postgres database that you want to use, let's create a new database on Heroku and attach it to our app:

heroku addons:create --app <app-name> heroku-postgresql:mini
caution

Heroku does not offer a free plan anymore and mini is their cheapest database instance - it costs $5/mo.

Heroku will also set DATABASE_URL env var for us at this point. If you are using an external database, you will have to set it up yourself.

The PORT env var will also be provided by Heroku, so the only two left to set are the JWT_SECRET and WASP_WEB_CLIENT_URL env vars:

heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
note

If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

Deploy to a Heroku App

After you have built the app, position yourself in .wasp/build/ directory:

cd .wasp/build

assuming you were at the root of your Wasp project at that moment.

Log in to Heroku Container Registry:

heroku container:login

Build the docker image and push it to Heroku:

heroku container:push --app <app-name> web

App is still not deployed at this point. This step might take some time, especially the very first time, since there are no cached docker layers.

Note for Apple Silicon Users

Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of heroku container:push, users instead should:

docker buildx build --platform linux/amd64 -t <app-name> .
docker tag <app-name> registry.heroku.com/<app-name>/web
docker push registry.heroku.com/<app-name>/web

You are now ready to proceed to the next step.

Deploy the pushed image and restart the app:

heroku container:release --app <app-name> web

This is it, the backend is deployed at https://<app-name>-XXXX.herokuapp.com 🎉

Find out the exact app URL with:

heroku info --app <app-name>

Additionally, you can check out the logs with:

heroku logs --tail --app <app-name>
Using pg-boss with Heroku

If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.

Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/deployment/overview.html b/docs/0.11.8/advanced/deployment/overview.html index 5aa709928c..215e7d4a65 100644 --- a/docs/0.11.8/advanced/deployment/overview.html +++ b/docs/0.11.8/advanced/deployment/overview.html @@ -19,8 +19,8 @@ - - + +
@@ -29,7 +29,7 @@ It also runs any pending migrations.

You can add extra steps to this multi-stage Dockerfile by creating your own Dockerfile in the project's root directory. If Wasp finds a Dockerfile in the project's root, it appends its contents at the bottom of the default multi-stage Dockerfile.

Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

A few things to keep in mind:

  • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
  • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
  • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

Read more in the official Docker docs on multi-stage builds.

To see what your project's (potentially combined) Dockerfile will look like, run:

wasp dockerfile

Join our Discord if you have any questions, or if you need more customization than this hook provides.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/email.html b/docs/0.11.8/advanced/email.html index 1e9664f94c..41b7ce5fbe 100644 --- a/docs/0.11.8/advanced/email.html +++ b/docs/0.11.8/advanced/email.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Sending Emails

With Wasp's email sending feature, you can easily integrate email functionality into your web application.

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

Choose from one of the providers:

  • Mailgun,
  • SendGrid
  • or the good old SMTP.

Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

Sending Emails

Sending emails while developing

When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.

To enable sending emails in development mode, you need to set the SEND_EMAILS_IN_DEVELOPMENT env variable to true in your .env.server file.

Before jumping into details about setting up various providers, let's see how easy it is to send emails.

You import the emailSender that is provided by the @wasp/email module and call the send method on it.

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

Read more about the send method in the API Reference.

The send method returns an object with the status of the sent email. It varies depending on the provider you use.

Providers

For each provider, you'll need to set up env variables in the .env.server file at the root of your project.

Using the SMTP Provider

First, set the provider to SMTP in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SMTP,
}
}

Then, add the following env variables to your .env.server file.

.env.server
SMTP_HOST=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_PORT=

Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

Using the Mailgun Provider

Set the provider to Mailgun in the main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: Mailgun,
}
}

Then, get the Mailgun API key and domain and add them to your .env.server file.

Getting the API Key and Domain

  1. Go to Mailgun and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
  4. Go to Domains and create a new domain.
  5. Copy the domain and add it to your .env.server file.
.env.server
MAILGUN_API_KEY=
MAILGUN_DOMAIN=

Using the SendGrid Provider

Set the provider field to SendGrid in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SendGrid,
}
}

Then, get the SendGrid API key and add it to your .env.server file.

Getting the API Key

  1. Go to SendGrid and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
.env.server
SENDGRID_API_KEY=

API Reference

emailSender dict

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

The emailSender dict has the following fields:

  • provider: Provider required

    The provider you want to use. Choose from SMTP, Mailgun or SendGrid.

  • defaultFrom: dict

    The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

JavaScript API

Using the emailSender in :

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

The send method accepts an object with the following fields:

  • from: object

    The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

    • name: string

      The name of the sender.

    • email: string

      The email address of the sender.

  • to: string required

    The recipient's email address.

  • subject: string required

    The subject of the email.

  • text: string required

    The text version of the email.

  • html: string required

    The HTML version of the email

- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/jobs.html b/docs/0.11.8/advanced/jobs.html index fecb100dc1..83aaa9eb26 100644 --- a/docs/0.11.8/advanced/jobs.html +++ b/docs/0.11.8/advanced/jobs.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Recurring Jobs

In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

Wasp supports background jobs that can help you with this:

  • Jobs persist between server restarts,
  • Jobs can be retried if they fail,
  • Jobs can be delayed until a future time,
  • Jobs can have a recurring schedule.

Using Jobs

Job Definition and Usage

Let's write an example Job that will print a message to the console and return a list of tasks from the database.

  1. Start by creating a Job declaration in your .wasp file:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@server/workers/bar.js"
    },
    entities: [Task],
    }
  2. After declaring the Job, implement its worker function:

    bar.js
    export const foo = async ({ name }, context) => {
    console.log(`Hello ${name}!`)
    const tasks = await context.entities.Task.findMany({})
    return { tasks }
    }
    The worker function

    The worker function must be an async function. The function's return value represents the Job's result.

    The worker function accepts two arguments:

    • args: The data passed into the job when it's submitted.
    • context: { entities }: The context object containing entities you put in the Job declaration.
  3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'

    const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

    // Or, if you'd prefer it to execute in the future, just add a .delay().
    // It takes a number of seconds, Date, or ISO date string.
    await mySpecialJob
    .delay(10)
    .submit({ name: "Johnny" })

And that'is it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

Recurring Jobs

If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js"
},
schedule: {
cron: "0 * * * *",
args: {=json { "job": "args" } json=} // optional
}
}

In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

API Reference

Declaring Jobs

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js",
executorOptions: {
pgBoss: {=json { "retryLimit": 1 } json=}
}
},
schedule: {
cron: "*/5 * * * *",
args: {=json { "foo": "bar" } json=},
executorOptions: {
pgBoss: {=json { "retryLimit": 0 } json=}
}
},
entities: [Task],
}

The Job declaration has the following fields:

  • executor: JobExecutor required

    Job executors

    Our jobs need job executors to handle the scheduling, monitoring, and execution.

    PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires your app.db.system to be PostgreSQL.

    We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

    info

    Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

    pg-boss details

    pg-boss provides many useful features, which can be found here.

    When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

    If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

    pg-boss considerations

    • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
      • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
    • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
      • If you remove a schedule from a job, you will need to do the above as well.
    • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
    • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
  • perform: dict required

    • fn: ServerImport required

      • An async function that performs the work. Since Wasp executes Jobs on the server, you must import it from @server.
      • It receives the following arguments:
        • args: Input: The data passed to the job when it's submitted.
        • context: { entities: Entities }: The context object containing any declared entities.

      Here's an example of a perform.fn function:

      bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
    • executorOptions: dict

      Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

      • pgBoss: JSON

        See the docs for pg-boss.

  • schedule: dict

    • cron: string required

      A 5-placeholder format cron expression string. See rationale for minute-level precision here.

      If you need help building cron expressions, Check out Crontab guru.

    • args: JSON

      The arguments to pass to the perform.fn function when invoked.

    • executorOptions: dict

      Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

      • pgBoss: JSON

        See the docs for pg-boss.

  • entities: [Entity]

    A list of entities you wish to use inside your Job (similar to Queries and Actions).

JavaScript API

  • Importing a Job:

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
  • submit(jobArgs, executorOptions)

    • jobArgs: Input

    • executorOptions: object

      Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

    someAction.js
    const submittedJob = await mySpecialJob.submit({ job: "args" })
  • delay(startAfter)

    • startAfter: int | string | Date required

      Delaying the invocation of the job handler. The delay can be one of:

      • Integer: number of seconds to delay. [Default 0]
      • String: ISO date string to run at.
      • Date: Date to run at.
    someAction.js
    const submittedJob = await mySpecialJob
    .delay(10)
    .submit({ job: "args" }, { "retryLimit": 2 })

Tracking

The return value of submit() is an instance of SubmittedJob, which has the following fields:

  • jobId: The ID for the job in that executor.
  • jobName: The name of the job you used in your .wasp file.
  • executorName: The Symbol of the name of the job executor.
    • For pg-boss, you can import a Symbol from: import { PG_BOSS_EXECUTOR_NAME } from '@wasp/jobs/core/pgBoss/pgBossJob.js' if you wish to compare against executorName.

There are also some namespaced, job executor-specific objects.

  • For pg-boss, you may access: pgBoss
    • details(): pg-boss specific job detail information. Reference
    • cancel(): attempts to cancel a job. Reference
    • resume(): attempts to resume a canceled job. Reference
- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/links.html b/docs/0.11.8/advanced/links.html index ae4a0d37e4..29ec622cd5 100644 --- a/docs/0.11.8/advanced/links.html +++ b/docs/0.11.8/advanced/links.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Type-Safe Links

If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

After you defined a route:

main.wasp
route TaskRoute { path: "/task/:id", to: TaskPage }
page TaskPage { ... }

You can get the benefits of type-safe links by using the Link component from @wasp/router:

TaskList.tsx
import { Link } from '@wasp/router'

export const TaskList = () => {
// ...

return (
<div>
{tasks.map((task) => (
<Link
key={task.id}
to="/task/:id"
{/* 👆 You must provide a valid path here */}
params={{ id: task.id }}>
{/* 👆 All the params must be correctly passed in */}
{task.description}
</Link>
))}
</div>
)
}

Using Search Query & Hash

You can also pass search and hash props to the Link component:

TaskList.tsx
<Link
to="/task/:id"
params={{ id: task.id }}
search={{ sortBy: 'date' }}
hash="comments"
>
{task.description}
</Link>

This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

The routes Object

You can also get all the pages in your app with the routes object:

TaskList.tsx
import { routes } from '@wasp/router'

const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

This will result in a link like this: /task/1.

You can also pass search and hash props to the build function. Check out the API Reference for more details.

API Reference

The Link component accepts the following props:

  • to required

    • A valid Wasp Route path from your main.wasp file.
  • params: { [name: string]: string | number } required (if the path contains params)

    • An object with keys and values for each param in the path.
    • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
  • search: string[][] | Record<string, string> | string | URLSearchParams

    • Any valid input for URLSearchParams constructor.
    • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
  • hash: string

  • all other props that the react-router-dom's Link component accepts

routes Object

The routes object contains a function for each route in your app.

router.tsx
export const routes = {
// RootRoute has a path like "/"
RootRoute: {
build: (options?: {
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}) => // ...
},

// DetailRoute has a path like "/task/:id/:something?"
DetailRoute: {
build: (
options: {
params: { id: ParamValue; something?: ParamValue; },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
}
}

The params object is required if the route contains params. The search and hash parameters are optional.

You can use the routes object like this:

import { routes } from '@wasp/router'

const linkToRoot = routes.RootRoute.build()
const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/middleware-config.html b/docs/0.11.8/advanced/middleware-config.html index 74486fc523..7de6e65484 100644 --- a/docs/0.11.8/advanced/middleware-config.html +++ b/docs/0.11.8/advanced/middleware-config.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Configuring Middleware

Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

Default Global Middleware 🌍

Wasp's Express server has the following middleware by default:

  • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

  • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

    note

    CORS middleware is required for the frontend to communicate with the backend.

  • Morgan: HTTP request logger middleware.

  • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

    note

    JSON middlware is required for Operations to function properly.

  • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

  • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

Customization

You have three places where you can customize middleware:

  1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

    Modifying global middleware

    Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

  2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

  3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

    • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

Default Middleware Definitions

Below is the actual definitions of default middleware which you can override.

const defaultGlobalMiddleware = new Map([
['helmet', helmet()],
['cors', cors({ origin: config.allowedCORSOrigins })],
['logger', logger('dev')],
['express.json', express.json()],
['express.urlencoded', express.urlencoded({ extended: false })],
['cookieParser', cookieParser()]
])

1. Customize Global Middleware

If you would like to modify the middleware for all operations and APIs, you can do something like:

main.wasp
app todoApp {
// ...

server: {
setupFn: import setup from "@server/serverSetup.js",
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
},
}
src/server/serverSetup.js
import cors from 'cors'
import config from '@wasp/config.js'

export const serverMiddlewareFn = (middlewareConfig) => {
// Example of adding extra domains to CORS.
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
return middlewareConfig
}

2. Customize api-specific Middleware

If you would like to modify the middleware for a single API, you can do something like:

main.wasp
// ...

api webhookCallback {
fn: import { webhookCallback } from "@server/apis.js",
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
httpRoute: (POST, "/webhook/callback"),
auth: false
}
src/server/apis.js
import express from 'express'

export const webhookCallback = (req, res, _context) => {
res.json({ msg: req.body.length })
}

export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

middlewareConfig.delete('express.json')
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

return middlewareConfig
}

note

This gets installed on a per-method basis. Behind the scenes, this results in code like:

router.post('/webhook/callback', webhookCallbackMiddleware, ...)

3. Customize Per-Path Middleware

If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

main.wasp
// ...

apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo/bar"
}
src/server/apis.js
export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
const customMiddleware = (_req, _res, next) => {
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
next()
}

middlewareConfig.set('custom.middleware', customMiddleware)

return middlewareConfig
}
note

This gets installed at the router level for the path. Behind the scenes, this results in something like:

router.use('/foo/bar', fooBarNamespaceMiddleware)
- - + + \ No newline at end of file diff --git a/docs/0.11.8/advanced/web-sockets.html b/docs/0.11.8/advanced/web-sockets.html index 9bbd682cb6..fcb7639f09 100644 --- a/docs/0.11.8/advanced/web-sockets.html +++ b/docs/0.11.8/advanced/web-sockets.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Web Sockets

Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

To get started, you need to:

  1. Define your WebSocket logic on the server.
  2. Enable WebSockets in your Wasp file, and connect it with your server logic.
  3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
  4. Optionally, type the WebSocket events and payloads for full-stack type safety.

Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

Turn On WebSockets in Your Wasp File

We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

Defining the Events Handler

Let's define the WebSockets server with all of the events and handler functions.

webSocketFn Function

On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

This is how we can define our webSocketFn function:

src/server/webSocket.js
import { v4 as uuidv4 } from 'uuid'

export const webSocketFn = (io, context) => {
io.on('connection', (socket) => {
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
console.log('a user connected: ', username)

socket.on('chatMessage', async (msg) => {
console.log('message: ', msg)
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
// You can also use your entities here:
// await context.entities.SomeEntity.create({ someField: msg })
})
})
}

Using the WebSocket On The Client

useSocket Hook

Client access to WebSockets is provided by the useSocket hook. It returns:

  • socket: Socket for sending and receiving events.
  • isConnected: boolean for showing a display of the Socket.IO connection status.
    • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
    • If you set autoConnect: false in your Wasp file, then you should call these as needed.

All components using useSocket share the same underlying socket.

useSocketListener Hook

Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

src/client/ChatPage.jsx
import React, { useState } from 'react'
import {
useSocket,
useSocketListener,
} from '@wasp/webSocket'

export const ChatPage = () => {
const [messageText, setMessageText] = useState('')
const [messages, setMessages] = useState([])
const { socket, isConnected } = useSocket()

useSocketListener('chatMessage', logMessage)

function logMessage(msg) {
setMessages((priorMessages) => [msg, ...priorMessages])
}

function handleSubmit(e) {
e.preventDefault()
socket.emit('chatMessage', messageText)
setMessageText('')
}

const messageList = messages.map((msg) => (
<li key={msg.id}>
<em>{msg.username}</em>: {msg.text}
</li>
))
const connectionIcon = isConnected ? '🟢' : '🔴'

return (
<>
<h2>Chat {connectionIcon}</h2>
<div>
<form onSubmit={handleSubmit}>
<div>
<div>
<input
type="text"
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
/>
</div>
<div>
<button type="submit">Submit</button>
</div>
</div>
</form>
<ul>{messageList}</ul>
</div>
</>
)
}

API Reference

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

The webSocket dict has the following fields:

  • fn: WebSocketFn required

    The function that defines the WebSocket events and handlers.

  • autoConnect: bool

    Whether to automatically connect to the WebSocket server. Default: true.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/email.html b/docs/0.11.8/auth/email.html index eefc0ffcb5..fa6b56b17b 100644 --- a/docs/0.11.8/auth/email.html +++ b/docs/0.11.8/auth/email.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Email

Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

Auth UI

Using email auth and social auth together

If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the User entity), they'll be able to reset their password and login with e-mail and password ✅

If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that ❌

In the future, we will lift this limitation and enable smarter merging of accounts.

Setting Up Email Authentication

We'll need to take the following steps to set up email authentication:

  1. Enable email authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages
  5. Set up the email sender

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}

// Defining User entity
entity User { ... }

// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Email Authentication in main.wasp

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable email authentication
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options (we'll talk about them later)
emailVerification: {
clientRoute: EmailVerificationRoute,
},
passwordReset: {
clientRoute: PasswordResetRoute,
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
}

Read more about the email auth method options here.

2. Add the User Entity

When email authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 5. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...

// 6. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@client/pages/auth.jsx",
}

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@client/pages/auth.jsx",
}

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@client/pages/auth.jsx",
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { VerifyEmailForm } from "@wasp/auth/forms/VerifyEmail";
import { ForgotPasswordForm } from "@wasp/auth/forms/ForgotPassword";
import { ResetPasswordForm } from "@wasp/auth/forms/ResetPassword";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
<br />
<span className="text-sm font-medium text-gray-900">
Forgot your password? <Link to="/request-password-reset">reset it</Link>
.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

export function EmailVerification() {
return (
<Layout>
<VerifyEmailForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

export function RequestPasswordReset() {
return (
<Layout>
<ForgotPasswordForm />
</Layout>
);
}

export function PasswordReset() {
return (
<Layout>
<ResetPasswordForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

5. Set up an Email Sender

To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers.

To set up SendGrid to send emails, we will add the following to our main.wasp file:

main.wasp
app myApp {
// ...
// 7. Set up the email sender
emailSender: {
provider: SendGrid,
}
}

... and add the following to our .env.server file:

.env.server
SENDGRID_API_KEY=<your key>

If you are not sure how to get a SendGrid API key, read more here.

Read more about setting up email senders in the sending emails docs.

Conclusion

That's it! We have set up email authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Login and Signup Flows

Login

Auth UI

If logging in with an unverified email is allowed, the user will be able to login with an unverified email address. If logging in with an unverified email is not allowed, the user will be shown an error message.

Read more about the allowUnverifiedLogin option here.

Signup

Auth UI

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

  3. Allowing registration for unverified emails

    If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

  4. Password validation

    Read more about the default password validation rules and how to override them in using auth docs.

Email Verification Flow

By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

Our setup looks like this:

main.wasp
// ...

emailVerification: {
clientRoute: EmailVerificationRoute,
}

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

The content of the e-mail can be customized, read more about it here.

Email Verification Page

We defined our email verification page in the auth.tsx file.

Auth UI

Password Reset Flow

Users can request a password and then they'll receive an e-mail with a link to reset their password.

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

Our setup in main.wasp looks like this:

main.wasp
// ...

passwordReset: {
clientRoute: PasswordResetRoute,
}

Request Password Reset Page

Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

Request password reset page

Password Reset Page

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

Request password reset page

Users can enter their new password there.

The content of the e-mail can be customized, read more about it here.

Using The Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

Let's go over the options we can specify when using email authentication.

userEntity fields

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
// We'll explain these options below
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

// Using email auth requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
psl=}

Email auth requires that userEntity specified in auth contains:

  • optional email field of type String
  • optional password field of type String
  • isEmailVerified field of type Boolean with a default value of false
  • optional emailVerificationSentAt field of type DateTime
  • optional passwordResetSentAt field of type DateTime

Fields in the email dict

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
fromField: {
name: "My App",
email: "hello@itsme.com"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

fromField: EmailFromField required

fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

It has the following fields:

  • name: name of the sender
  • email: e-mail address of the sender required

emailVerification: EmailVerificationConfig required

emailVerification is a dict that specifies the details of the e-mail verification process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

    Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

    src/pages/EmailVerificationPage.jsx
    import { verifyEmail } from '@wasp/auth/email/actions';
    ...
    await verifyEmail({ token });
    note

    We used Auth UI above to avoid doing this work of sending the token to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn can be done by defining a file in the server directory.

    server/email.js
    export const getVerificationEmailContent = ({ verificationLink }) => ({
    subject: 'Verify your email',
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

passwordReset: PasswordResetConfig required

passwordReset is a dict that specifies the password reset process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to reset their password. required

    Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

    src/pages/ForgotPasswordPage.jsx
    import { requestPasswordReset } from '@wasp/auth/email/actions';
    ...
    await requestPasswordReset({ email });
    src/pages/PasswordResetPage.jsx
    import { resetPassword } from '@wasp/auth/email/actions';
    ...
    await resetPassword({ password, token })
    note

    We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn is done by defining a function that looks like this:

    server/email.js
    export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
    subject: 'Password reset',
    text: `Click the link below to reset your password: ${passwordResetLink}`,
    html: `
    <p>Click the link below to reset your password</p>
    <a href="${passwordResetLink}">Reset password</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

allowUnverifiedLogin: bool: specifies whether the user can login without verifying their e-mail address

It defaults to false. If allowUnverifiedLogin is set to true, the user can login without verifying their e-mail address, otherwise users will receive a 401 error when trying to login without verifying their e-mail address.

Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the isEmailVerified field on the user entity to check if the user has verified their e-mail address.

If you have any questions, feel free to ask them on our Discord server.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/overview.html b/docs/0.11.8/auth/overview.html index d042d66e60..3a706dd684 100644 --- a/docs/0.11.8/auth/overview.html +++ b/docs/0.11.8/auth/overview.html @@ -19,8 +19,8 @@ - - + +
@@ -29,7 +29,7 @@ For update(), they only run when the field mentioned in validates is present.

The validation process stops on the first validator to return false. If enabled, default validations run first and then custom validations.

Validation Error Handling

When creating, updating, or deleting entities, you may wish to handle validation errors. Wasp exposes a class called AuthError for this purpose.

src/server/actions.js
try {
await context.entities.User.update(...)
} catch (e) {
if (e instanceof AuthError) {
throw new HttpError(422, 'Validation failed', { message: e.message })
} else {
throw e
}
}

Customizing the Signup Process

Sometimes you want to include extra fields in your signup process, like first name and last name.

In Wasp, in this case:

  • you need to define the fields that you want saved in the database,
  • you need to customize the SignupForm.

Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.

Let's see how to do both.

1. Defining Extra Fields

If we want to save some extra fields in our signup process, we need to tell our app they exist.

We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

* We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

First, we add the auth.signup.additionalFields field in our main.wasp file:

main.wasp
app crudTesting {
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login",
signup: {
additionalFields: import { fields } from "@server/auth/signup.js",
},
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
address String?
psl=}

Then we'll define and export the fields object from the server/auth/signup.js file:

server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'

export const fields = defineAdditionalSignupFields({
address: async (data) => {
const address = data.address
if (typeof address !== 'string') {
throw new Error('Address is required')
}
if (address.length < 5) {
throw new Error('Address must be at least 5 characters long')
}
return address
},
})

Read more about the fields object in the API Reference.

Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity.

The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.

Using Validation Libraries

You can use any validation library you want to validate the fields. For example, you can use zod like this:

Click to see the code
server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
import * as z from 'zod'

export const fields = defineAdditionalSignupFields({
address: (data) => {
const AddressSchema = z
.string({
required_error: 'Address is required',
invalid_type_error: 'Address must be a string',
})
.min(10, 'Address must be at least 10 characters long')
const result = AddressSchema.safeParse(data.address)
if (result.success === false) {
throw new Error(result.error.issues[0].message)
}
return result.data
},
})

Now that we defined the fields, Wasp knows how to:

  1. Validate the data sent from the client
  2. Save the data to the database

Next, let's see how to customize Auth UI to include those fields.

2. Customizing the Signup Component

Using Custom Signup Component

If you are not using Wasp's Auth UI, you can skip this section. Just make sure to include the extra fields in your custom signup form.

Read more about using the signup actions for:

  • email auth here
  • username & password auth here

If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.

Using a List of Extra Fields

When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.

Inside the list, there can be either objects or render functions (you can combine them):

  1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
  2. Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the react-hook-form object and the form state object as arguments.
client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import {
FormError,
FormInput,
FormItemGroup,
FormLabel,
} from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={[
/* The address field is defined using an object */
{
name: 'address',
label: 'Address',
type: 'input',
validations: {
required: 'Address is required',
},
},
/* The phone number is defined using a render function */
(form, state) => {
return (
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber', {
required: 'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber && (
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}

Read more about the extra fields in the API Reference.

Using a Single Render Function

Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.

client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import { FormItemGroup } from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={(form, state) => {
const username = form.watch('username')
return (
username && (
<FormItemGroup>
Hello there <strong>{username}</strong> 👋
</FormItemGroup>
)
)
}}
/>
)
}

Read more about the render function in the API Reference.

API Reference

Auth Fields

main.wasp
  title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute",
signup: { ... }
}
}

//...

app.auth is a dictionary with the following fields:

userEntity: entity required

The entity representing the user. Its mandatory fields depend on your chosen auth method.

externalAuthEntity: entity

Wasp requires you to set the field auth.externalAuthEntity for all authentication methods relying on an external authorizatino provider (e.g., Google). You also need to tweak the Entity referenced by auth.userEntity, as shown below.

main.wasp
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
//...

entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
note

The same externalAuthEntity can be used across different social login providers (e.g., both GitHub and Google can use the same entity).

See Google docs and GitHub docs for more details.

methods: dict required

A dictionary of auth methods enabled for the app.

Click on each auth method for more details.

onAuthFailedRedirectTo: String required

The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true). Check out these essentials docs on auth to see an example of usage.

onAuthSucceededRedirectTo: String

The route to which Wasp will send a successfully authenticated after a successful login/signup. The default value is "/".

note

Automatic redirect on successful login only works when using the Wasp-provided Auth UI.

signup: SignupOptions

Read more about the signup process customization API in the Signup Fields Customization section.

Signup Fields Customization

If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.signup.additionalFields field in your main.wasp file.

main.wasp
app crudTesting {
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login",
signup: {
additionalFields: import { fields } from "@server/auth/signup.js",
},
},
}

Then we'll export the fields object from the server/auth/signup.js file:

server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'

export const fields = defineAdditionalSignupFields({
address: async (data) => {
const address = data.address
if (typeof address !== 'string') {
throw new Error('Address is required')
}
if (address.length < 5) {
throw new Error('Address must be at least 5 characters long')
}
return address
},
})

The fields object is an object where the keys represent the field name, and the values are functions which receive the data sent from the client* and return the value of the field.

If the field value is invalid, the function should throw an error.

* We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

SignupForm Customization

To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.

client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import {
FormError,
FormInput,
FormItemGroup,
FormLabel,
} from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={[
{
name: 'address',
label: 'Address',
type: 'input',
validations: {
required: 'Address is required',
},
},
(form, state) => {
return (
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber', {
required: 'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber && (
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}

The extra fields can be either objects or render functions (you can combine them):

  1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.

    The objects have the following properties:

    • name required

      • the name of the field
    • label required

      • the label of the field (used in the UI)
    • type required

      • the type of the field, which can be input or textarea
    • validations

      • an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
  2. Render functions receive the react-hook-form object and the form state as arguments, and they can use them to render arbitrary UI elements.

    The render function has the following signature:

    (form: UseFormReturn, state: FormState) => React.ReactNode
    • form required

      • the react-hook-form object, read more about it in the react-hook-form docs
      • you need to use the form.register function to register your fields
    • state required

      • the form state object which has the following properties:
        • isLoading: boolean
          • whether the form is currently submitting
- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/social-auth/github.html b/docs/0.11.8/auth/social-auth/github.html index e3b6bc387d..c243bb4b31 100644 --- a/docs/0.11.8/auth/social-auth/github.html +++ b/docs/0.11.8/auth/social-auth/github.html @@ -19,8 +19,8 @@ - - + +
@@ -29,7 +29,7 @@ To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

Default Behaviour

Add gitHub: {} to the auth.methods dictionary to use it with default settings.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

Also, if the userEntity has:

  • A username field: Wasp sets it to a random username (e.g. nice-blue-horse-14357).
  • A password field: Wasp sets it to a random string.

This is a historical coupling between auth methods we plan to remove in the future.

Overrides

Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.

There are two mechanisms (functions) used for overriding the default behavior:

  • getUserFieldsFn
  • configFn

Let's explore them in more detail.

Using the User's Provider Account Details

When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the getUserFieldsFn function.

For example, the User entity can include a displayName field which you can set based on the details received from the provider.

Wasp also lets you customize the configuration of the providers' settings using the configFn function.

Let's use this example to show both functions in action:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {
configFn: import { getConfig } from "@server/auth/github.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
displayName String
externalAuthAssociations SocialLogin[]
psl=}

// ...
src/server/auth/github.js
import { generateAvailableDictionaryUsername } from "@wasp/core/auth.js";

export const getUserFields = async (_context, args) => {
const username = await generateAvailableDictionaryUsername();
const displayName = args.profile.displayName;
return { username, displayName };
};

export function getConfig() {
return {
clientID // look up from env or elsewhere
clientSecret // look up from env or elsewhere
scope: [],
};
}

Using Auth

To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

API Reference

Provider-specific behavior comes down to implementing two functions.

  • configFn
  • getUserFieldsFn

The reference shows how to define both.

For behavior common to all providers, check the general API Reference.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {
configFn: import { getConfig } from "@server/auth/github.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

The gitHub dict has the following properties:

  • configFn: ServerImport

    This function should return an object with the Client ID, Client Secret, and scope for the OAuth provider.

    src/server/auth/github.js
    export function getConfig() {
    return {
    clientID, // look up from env or elsewhere
    clientSecret, // look up from env or elsewhere
    scope: [],
    }
    }
  • getUserFieldsFn: ServerImport

    This function should return the user fields to use when creating a new user.

    The context contains the User entity, and the args object contains GitHub profile information. You can do whatever you want with this information (e.g., generate a username).

    Here is how you could generate a username based on the Github display name:

    src/server/auth/github.js
    import { generateAvailableUsername } from '@wasp/core/auth.js'

    export const getUserFields = async (_context, args) => {
    const username = await generateAvailableUsername(
    args.profile.displayName.split(' '),
    { separator: '.' }
    )
    return { username }
    }

    Wasp exposes two functions that can help you generate usernames. Import them from @wasp/core/auth.js:

    • generateAvailableUsername takes an array of strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Github user Jim Smith.
    • generateAvailableDictionaryUsername generates a random dictionary phrase that is not yet in the database. For example, nice-blue-horse-27160.
- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/social-auth/google.html b/docs/0.11.8/auth/social-auth/google.html index 979668ea39..1634f416ad 100644 --- a/docs/0.11.8/auth/social-auth/google.html +++ b/docs/0.11.8/auth/social-auth/google.html @@ -19,8 +19,8 @@ - - + +
@@ -30,7 +30,7 @@ To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

Default Behaviour

Add google: {} to the auth.methods dictionary to use it with default settings:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {}
},
onAuthFailedRedirectTo: "/login"
},
}

When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

Also, if the userEntity has:

  • A username field: Wasp sets it to a random username (e.g. nice-blue-horse-14357).
  • A password field: Wasp sets it to a random string.

This is a historical coupling between auth methods we plan to remove in the future.

Overrides

Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.

There are two mechanisms (functions) used for overriding the default behavior:

  • getUserFieldsFn
  • configFn

Let's explore them in more detail.

Using the User's Provider Account Details

When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the getUserFieldsFn function.

For example, the User entity can include a displayName field which you can set based on the details received from the provider.

Wasp also lets you customize the configuration of the providers' settings using the configFn function.

Let's use this example to show both functions in action:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {
configFn: import { getConfig } from "@server/auth/google.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
displayName String
externalAuthAssociations SocialLogin[]
psl=}

// ...
src/server/auth/google.js
import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js'

export const getUserFields = async (_context, args) => {
const username = await generateAvailableDictionaryUsername()
const displayName = args.profile.displayName
return { username, displayName }
}

export function getConfig() {
return {
clientID, // look up from env or elsewhere
clientSecret, // look up from env or elsewhere
scope: ['profile', 'email'],
}
}

Using Auth

To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

API Reference

Provider-specific behavior comes down to implementing two functions.

  • configFn
  • getUserFieldsFn

The reference shows how to define both.

For behavior common to all providers, check the general API Reference.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {
configFn: import { getConfig } from "@server/auth/google.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

The google dict has the following properties:

  • configFn: ServerImport

    This function must return an object with the Client ID, the Client Secret, and the scope for the OAuth provider.

    src/server/auth/google.js
    export function getConfig() {
    return {
    clientID, // look up from env or elsewhere
    clientSecret, // look up from env or elsewhere
    scope: ['profile', 'email'],
    }
    }
  • getUserFieldsFn: ServerImport

    This function must return the user fields to use when creating a new user.

    The context contains the User entity, and the args object contains Google profile information. You can do whatever you want with this information (e.g., generate a username).

    Here is how to generate a username based on the Google display name:

    src/server/auth/google.js
    import { generateAvailableUsername } from '@wasp/core/auth.js'

    export const getUserFields = async (_context, args) => {
    const username = await generateAvailableUsername(
    args.profile.displayName.split(' '),
    { separator: '.' }
    )
    return { username }
    }

    Wasp exposes two functions that can help you generate usernames. Import them from @wasp/core/auth.js:

    • generateAvailableUsername takes an array of strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Github user Jim Smith.
    • generateAvailableDictionaryUsername generates a random dictionary phrase that is not yet in the database. For example, nice-blue-horse-27160.
- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/social-auth/overview.html b/docs/0.11.8/auth/social-auth/overview.html index 0ccded3756..86ed512d04 100644 --- a/docs/0.11.8/auth/social-auth/overview.html +++ b/docs/0.11.8/auth/social-auth/overview.html @@ -19,8 +19,8 @@ - - + +
@@ -34,7 +34,7 @@ If you're looking for the fastest way to get your auth up and running, that's where you should look.

The UI helpers described below are lower-level and are useful for creating your custom forms.

Wasp provides sign-in buttons and URLs for each of the supported social login providers.

client/LoginPage.jsx
import {
SignInButton as GoogleSignInButton,
signInUrl as googleSignInUrl,
} from '@wasp/auth/helpers/Google'
import {
SignInButton as GitHubSignInButton,
signInUrl as gitHubSignInUrl,
} from '@wasp/auth/helpers/GitHub'

export const LoginPage = () => {
return (
<>
<GoogleSignInButton />
<GitHubSignInButton />
{/* or */}
<a href={googleSignInUrl}>Sign in with Google</a>
<a href={gitHubSignInUrl}>Sign in with GitHub</a>
</>
)
}

If you need even more customization, you can create your custom components using signInUrls.

API Reference

Fields in the app.auth Dictionary and Overrides

For more information on:

  • Allowed fields in app.auth
  • getUserFields and configFn functions

Check the provider-specific API References:

The externalAuthEntity and Its Fields

Using social login providers requires you to define an External Auth Entity and declare it with the app.auth.externalAuthEntity field. This Entity holds the data relevant to the social provider. All social providers share the same Entity.

main.wasp
// ...

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

// ...
info

You don't need to know these details, you can just copy and paste the entity definition above and you are good to go.

The Entity acting as app.auth.externalAuthEntity must include the following fields:

  • provider - The provider's name (e.g. google, github, etc.).
  • providerId - The user's ID on the provider's platform.
  • userId - The user's ID on your platform (this references the id field from the Entity acting as app.auth.userEntity).
  • user - A relation to the userEntity (see the userEntity section) for more details.
  • createdAt - A timestamp of when the association was created.
  • @@unique([provider, providerId, userId]) - A unique constraint on the combination of provider, providerId and userId.

Expected Fields on the userEntity

Using Social login providers requires you to add one extra field to the Entity acting as app.auth.userEntity:

main.wasp
// ...

entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}

// ...
- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/ui.html b/docs/0.11.8/auth/ui.html index ad84b1a4f2..10d5021c6d 100644 --- a/docs/0.11.8/auth/ui.html +++ b/docs/0.11.8/auth/ui.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Auth UI

To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

Below we cover all of the available UI components and how to use them.

Auth UI

Overview

After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
},
// ...
}
}

You'll get the following UI:

Auth UI

And then if you enable Google and Github:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
google: {},
github: {},
},
// ...
}
}

The form will automatically update to look like this:

Auth UI

Let's go through all of the available components and how to use them.

Auth Components

The following components are available for you to use in your app:

Login Form

Used with Username & Password, Email, Github and Google authentication.

Login form

You can use the LoginForm component to build your login page:

main.wasp
// ...

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@client/LoginPage.jsx"
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

// Use it like this
export function LoginPage() {
return <LoginForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Signup Form

Used with Username & Password, Email, Github and Google authentication.

Signup form

You can use the SignupForm component to build your signup page:

main.wasp
// ...

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@client/SignupPage.jsx"
}
client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'

// Use it like this
export function SignupPage() {
return <SignupForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Read more about customizing the signup process like adding additional fields or extra UI in the Using Auth section.

Forgot Password Form

Used with Email authentication.

If users forget their password, they can use this form to reset it.

Forgot password form

You can use the ForgotPasswordForm component to build your own forgot password page:

main.wasp
// ...

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { ForgotPasswordPage } from "@client/ForgotPasswordPage.jsx"
}
client/ForgotPasswordPage.jsx
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'

// Use it like this
export function ForgotPasswordPage() {
return <ForgotPasswordForm />
}

Reset Password Form

Used with Email authentication.

After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

Reset password form

You can use the ResetPasswordForm component to build your reset password page:

main.wasp
// ...

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { ResetPasswordPage } from "@client/ResetPasswordPage.jsx"
}
client/ResetPasswordPage.jsx
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'

// Use it like this
export function ResetPasswordPage() {
return <ResetPasswordForm />
}

Verify Email Form

Used with Email authentication.

After users sign up, they will receive an email with a link to this form where they can verify their email.

Verify email form

You can use the VerifyEmailForm component to build your email verification page:

main.wasp
// ...

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { VerifyEmailPage } from "@client/VerifyEmailPage.jsx"
}
client/VerifyEmailPage.jsx
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'

// Use it like this
export function VerifyEmailPage() {
return <VerifyEmailForm />
}

Customization 💅🏻

You customize all of the available forms by passing props to them.

Props you can pass to all of the forms:

  1. appearance - customize the form colors (via design tokens)
  2. logo - path to your logo
  3. socialLayout - layout of the social buttons, which can be vertical or horizontal

1. Customizing the Colors

We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

List of all available tokens

See the list of all available tokens which you can override.

client/appearance.js
export const authAppearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import { authAppearance } from './appearance'

export function LoginPage() {
return (
<LoginForm
// Pass the appearance object to the form
appearance={authAppearance}
/>
)
}

We recommend defining your appearance in a separate file and importing it into your components.

You can add your logo to the Auth UI by passing the logo prop to any of the components.

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import Logo from './logo.png'

export function LoginPage() {
return (
<LoginForm
// Pass in the path to your logo
logo={Logo}
/>
)
}

3. Social Buttons Layout

You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

If we pass in vertical:

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

export function LoginPage() {
return (
<LoginForm
// Pass in the socialLayout prop
socialLayout="vertical"
/>
)
}

We get this:

Vertical social buttons

Let's Put Everything Together 🪄

If we provide the logo and custom colors:

client/appearance.js
export const appearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

import { authAppearance } from './appearance'
import todoLogo from './todoLogo.png'

export function LoginPage() {
return <LoginForm appearance={appearance} logo={todoLogo} />
}

We get a form looking like this:

Custom login form
- - + + \ No newline at end of file diff --git a/docs/0.11.8/auth/username-and-pass.html b/docs/0.11.8/auth/username-and-pass.html index 736ba96db2..41c109a5d7 100644 --- a/docs/0.11.8/auth/username-and-pass.html +++ b/docs/0.11.8/auth/username-and-pass.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Username & Password

Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.

Setting Up Username & Password Authentication

To set up username authentication we need to:

  1. Enable username authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}
// Defining User entity
entity User { ... }
// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Username Authentication

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

Read more about the usernameAndPassword auth method options here.

2. Add the User Entity

When username authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 3. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...
// 4. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

Conclusion

That's it! We have set up username authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Customizing the Auth Flow

The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

Read more about the default username and password validation rules and how to override them in the using auth docs.

If you require more control in your authentication flow, you can achieve that in the following ways:

  1. Create your UI and use signup and login actions.
  2. Create your custom sign-up and login actions which uses the Prisma client, along with your custom code.

1. Using the signup and login actions

login()

An action for logging in the user.

It takes two arguments:

  • username: string required

    Username of the user logging in.

  • password: string required

    Password of the user logging in.

You can use it like this:

client/pages/auth.jsx
// Importing the login action 👇
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function LoginPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await login(username, password)
history.push('/')
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}
note

When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

signup()

An action for signing up the user. This action does not log in the user, you still need to call login().

It takes one argument:

  • userFields: object required

    It has the following fields:

    • username: string required

    • password: string required

    info

    Wasp only stores the auth-related fields of the user entity. Adding extra fields to userFields will not have any effect.

    If you need to add extra fields to the user entity, we suggest doing it in a separate step after the user logs in for the first time.

You can use it like this:

client/pages/auth.jsx
// Importing the signup and login actions 👇
import signup from '@wasp/auth/signup'
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function Signup() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await signup({
username,
password,
})
await login(username, password)
history.push("/")
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}

2. Creating your custom actions

The code of your custom sign-up action can look like this:

main.wasp
// ...

action signupUser {
fn: import { signUp } from "@server/auth/signup.js",
entities: [User]
}
src/server/auth/signup.js
export const signUp = async (args, context) => {
// Your custom code before sign-up.
// ...

const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
}
})

// Your custom code after sign-up.
// ...
return newUser
}

Using Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

userEntity fields

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

// Wasp requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

Username & password auth requires that userEntity specified in auth contains:

  • username field of type String
  • password field of type String

Fields in the usernameAndPassword dict

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
// ...
info

usernameAndPassword dict doesn't have any options at the moment.

You can read about the rest of the auth options in the using auth section of the docs.

- - + + \ No newline at end of file diff --git a/docs/0.11.8/contact.html b/docs/0.11.8/contact.html index 825b0379a9..213203d862 100644 --- a/docs/0.11.8/contact.html +++ b/docs/0.11.8/contact.html @@ -19,13 +19,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/0.11.8/contributing.html b/docs/0.11.8/contributing.html index 142e492b9b..de8ab57d61 100644 --- a/docs/0.11.8/contributing.html +++ b/docs/0.11.8/contributing.html @@ -19,13 +19,13 @@ - - + +
Version: 0.11.8

Contributing

Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

Some side notes to make your journey easier:

  1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

  2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

  3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

Happy hacking!

- - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/backends.html b/docs/0.11.8/data-model/backends.html index 9ff2f0f28f..fbf313fdc6 100644 --- a/docs/0.11.8/data-model/backends.html +++ b/docs/0.11.8/data-model/backends.html @@ -19,8 +19,8 @@ - - + +
@@ -36,7 +36,7 @@ Whenever you modify the db.system field, make sure to run wasp db migrate-dev to apply the changes.

  • seeds: [ServerImport]

    Defines the seed functions you can use with the wasp db seed command to seed your database with initial data. Read the Seeding section for more details.

  • prisma: PrismaOptions

    Additional configuration for Prisma.

    main.wasp
    app MyApp {
    // ...
    db: {
    // ...
    prisma: {
    clientPreviewFeatures: ["postgresqlExtensions"],
    dbExtensions: [
    { name: "hstore", schema: "myHstoreSchema" },
    { name: "pg_trgm" },
    { name: "postgis", version: "2.1" },
    ]
    }
    }
    }

    It's a dictionary with the following fields:

    • clientPreviewFeatures : [string]

      Allows you to define Prisma client preview features, like for example, "postgresqlExtensions".

    • dbExtensions: DbExtension[]

      It allows you to define PostgreSQL extensions that should be enabled for your database. Read more about PostgreSQL extensions in Prisma.

      For each extension you define a dict with the following fields:

      • name: string required

        The name of the extension you would normally put in the Prisma file.

        schema.prisma
        extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
        // 👆 Extension name
      • map: string

        It sets the map argument of the extension. Explanation for the field from the Prisma docs:

        This is the database name of the extension. If this argument is not specified, the name of the extension in the Prisma schema must match the database name.

      • schema: string

        It sets the schema argument of the extension. Explanation for the field from the Prisma docs:

        This is the name of the schema in which to activate the extension's objects. If this argument is not specified, the current default object creation schema is used.

      • version: string

        It sets the version argument of the extension. Explanation for the field from the Prisma docs:

        This is the version of the extension to activate. If this argument is not specified, the value given in the extension's control file is used.

  • CLI Commands for Seeding the Database

    Use one of the following commands to run the seed functions:

    • wasp db seed

      If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.

    • wasp db seed <seed-name>

      This command runs the seed function with the specified name. The name is the identifier used in its import expression in the app.db.seeds list. For example, to run the seed function devSeedSimple which was defined like this:

      main.wasp
      app MyApp {
      // ...
      db: {
      // ...
      seeds: [
      // ...
      import { devSeedSimple } from "@server/dbSeeds.js",
      ]
      }
      }

      Use the following command:

      wasp db seed devSeedSimple
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/crud.html b/docs/0.11.8/data-model/crud.html index 175724b55c..84e4b01803 100644 --- a/docs/0.11.8/data-model/crud.html +++ b/docs/0.11.8/data-model/crud.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    Automatic CRUD

    If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.

    Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.

    With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting Entities. As you update definitions for your Entities, Wasp automatically regenerates the backend logic.

    Early preview

    This feature is currently in early preview and we are actively working on it. Read more about our plans for CRUD operations.

    Overview

    Imagine we have a Task entity and we want to enable CRUD operations for it.

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    psl=}

    We can then define a new crud called Tasks.

    We specify to use the Task entity and we enable the getAll, get, create and update operations (let's say we don't need the delete operation).

    main.wasp
    crud Tasks {
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // by default only logged in users can perform operations
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    update: {},
    },
    }
    1. It uses default implementation for getAll, get, and update,
    2. ... while specifying a custom implementation for create.
    3. getAll will be public (no auth needed), while the rest of the operations will be private.

    Here's what it looks like when visualized:

    Automatic CRUD with Wasp
    Visualization of the Tasks crud declaration

    We can now use the CRUD queries and actions we just specified in our client code.

    Keep reading for an example of Automatic CRUD in action, or skip ahead for the API Reference

    Example: A Simple TODO App

    Let's create a full-app example that uses automatic CRUD. We'll stick to using the Task entity from the previous example, but we'll add a User entity and enable username and password based auth.

    Automatic CRUD with Wasp
    We are building a simple tasks app with username based auth

    Creating the App

    We can start by running wasp new tasksCrudApp and then adding the following to the main.wasp file:

    main.wasp
    app tasksCrudApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Tasks Crud App",

    // We enabled auth and set the auth method to username and password
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    tasks Task[]
    psl=}

    // We defined a Task entity on which we'll enable CRUD later on
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    psl=}

    // Tasks app routes
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@client/MainPage.jsx",
    authRequired: true,
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@client/LoginPage.jsx",
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@client/SignupPage.jsx",
    }

    We can then run wasp db migrate-dev to create the database and run the migrations.

    Adding CRUD to the Task Entity ✨

    Let's add the following crud declaration to our main.wasp file:

    main.wasp
    // ...

    crud Tasks {
    entity: Task,
    operations: {
    getAll: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    },
    }

    You'll notice that we enabled only getAll and create operations. This means that only these operations will be available.

    We also overrode the create operation with a custom implementation. This means that the create operation will not be generated, but instead, the createTask function from @server/tasks.js will be used.

    Our Custom create Operation

    Here's the src/server/tasks.ts file:

    src/server/tasks.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    We made a custom create operation because we want to make sure that the task is connected to the user that is creating it. Automatic CRUD doesn't support this by default (yet!). Read more about the default implementations here.

    Using the Generated CRUD Operations on the Client

    And let's use the generated operations in our client code:

    pages/MainPage.jsx
    import { Tasks } from '@wasp/crud/Tasks'
    import { useState } from 'react'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
    const createTask = Tasks.create.useAction()
    const [taskDescription, setTaskDescription] = useState('')

    function handleCreateTask() {
    createTask({ description: taskDescription, isDone: false })
    setTaskDescription('')
    }

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    return (
    <div
    style={{
    fontSize: '1.5rem',
    display: 'grid',
    placeContent: 'center',
    height: '100vh',
    }}
    >
    <div>
    <input
    value={taskDescription}
    onChange={(e) => setTaskDescription(e.target.value)}
    />
    <button onClick={handleCreateTask}>Create task</button>
    </div>
    <ul>
    {tasks.map((task) => (
    <li key={task.id}>{task.description}</li>
    ))}
    </ul>
    </div>
    )
    }

    And here are the login and signup pages, where we are using Wasp's Auth UI components:

    src/client/LoginPage.jsx
    import { LoginForm } from '@wasp/auth/forms/Login'
    import { Link } from 'react-router-dom'

    export function LoginPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <LoginForm />
    <div>
    <Link to="/signup">Create an account</Link>
    </div>
    </div>
    )
    }
    src/client/SignupPage.jsx
    import { SignupForm } from '@wasp/auth/forms/Signup'

    export function SignupPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <SignupForm />
    </div>
    )
    }

    That's it. You can now run wasp start and see the app in action. ⚡️

    You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.

    Future of CRUD Operations in Wasp

    CRUD operations currently have a limited set of knowledge about the business logic they are implementing.

    • For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the create operation in the example above.
    • Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
    • Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.

    CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.

    We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on this GitHub issue to see how we are doing.

    API Reference

    CRUD declaration work on top of existing entity declaration. We'll fully explore the API using two examples:

    1. A basic CRUD declaration that relies on default options.
    2. A more involved CRUD declaration that uses extra options and overrides.

    Declaring a CRUD With Default Options

    If we create CRUD operations for an entity named Task, like this:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    get: {},
    getAll: {},
    create: {},
    update: {},
    delete: {},
    },
    }

    Wasp will give you the following default implementations:

    get - returns one entity based on the id field

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.findUnique({ where: { id: args.id } })

    getAll - returns all entities

    // ...

    // If the operation is not public, Wasp checks if an authenticated user
    // is making the request.

    return Task.findMany()

    create - creates a new entity

    // ...
    return Task.create({ data: args.data })

    update - updates an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.update({ where: { id: args.id }, data: args.data })

    delete - deletes an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.delete({ where: { id: args.id } })
    Current Limitations

    In the default create and update implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.

    In the future, we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.

    For now, the solution is to provide an override function. You can override the default implementation by using the overrideFn option and implementing the validation logic yourself.

    Declaring a CRUD With All Available Options

    Here's an example of a more complex CRUD declaration:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // optional, defaults to false
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js", // optional
    },
    update: {},
    },
    }

    The CRUD declaration features the following fields:

    • entity: Entity required

      The entity to which the CRUD operations will be applied.

    • operations: { [operationName]: CrudOperationOptions } required

      The operations to be generated. The key is the name of the operation, and the value is the operation configuration.

      • The possible values for operationName are:
        • getAll
        • get
        • create
        • update
        • delete
      • CrudOperationOptions can have the following fields:
        • isPublic: bool - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to false.
        • overrideFn: ServerImport - The import statement of the optional override implementation in Node.js.

    Defining the overrides

    Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:

    • args

      The arguments of the operation i.e. the data sent from the client.

    • context

      Context contains the user making the request and the entities object with the entity that's being operated on.

    For a usage example, check the example guide.

    Using the CRUD operations in client code

    On the client, you import the CRUD operations from @wasp/crud/{crud name}. The names of the imports are the same as the names of the operations. For example, if you have a CRUD called Tasks, you would import the operations like this:

    SomePage.jsx
    import { Tasks } from '@wasp/crud/Tasks'

    You can then access the operations like this:

    SomePage.jsx
    const { data } = Tasks.getAll.useQuery()
    const { data } = Tasks.get.useQuery({ id: 1 })
    const createAction = Tasks.create.useAction()
    const updateAction = Tasks.update.useAction()
    const deleteAction = Tasks.delete.useAction()

    All CRUD operations are implemented with Queries and Actions under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)


    Join our community on Discord, where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/entities.html b/docs/0.11.8/data-model/entities.html index e5bc523d2e..b6935c6950 100644 --- a/docs/0.11.8/data-model/entities.html +++ b/docs/0.11.8/data-model/entities.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. Wasp Entities directly correspond to Prisma's data model. Still, you don't need to be familiar with Prisma to effectively use Wasp, as it comes with a simple API wrapper for working with Prisma's core features.

    The only requirement for defining Wasp Entities is familiarity with the Prisma Schema Language (PSL), a simple definition language explicitly created for defining models in Prisma. The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:

    Defining an Entity

    As mentioned, an entity declaration represents a database model.

    Each Entity declaration corresponds 1-to-1 to Prisma's data model. Here's how you could define an Entity that represents a Task:

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}

    Let's go through this declaration in detail:

    • entity Task - This tells Wasp that we wish to define an Entity (i.e., database model) called Task. Wasp automatically creates a table called tasks.
    • {=psl ... psl=} - Wasp treats everything that comes between the two psl tags as PSL (Prisma Schema Language).

    The above PSL definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the tasks table has three columns):

    • id - An integer value serving as a primary key. The database automatically generates it by incrementing the previously generated id.
    • description - A string value for storing the task's description.
    • isDone - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to false by default.

    Working with Entities

    Let's see how you can define and work with Wasp Entities:

    1. Create/update some Entities in your .wasp file.
    2. Run wasp db migrate-dev. This command syncs the database model with the Entity definitions in your .wasp file. It does this by creating migration scripts.
    3. Migration scripts are automatically placed in the migrations/ folder. Make sure to commit this folder into version control.
    4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about operations).

    Using Entities in Operations

    Most of the time, you will be working with Entities within the context of Operations (Queries & Actions). We'll see how that's done on the next page.

    Using Entities directly

    If you need more control, you can directly interact with Entities by importing and using the Prisma Client. We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.

    You can only use the Prisma Client in your Wasp server code. You can import it like this:

    import prismaClient from '@wasp/dbClient'`

    prismaClient.task.create({
    description: "Read the Entities doc",
    isDone: true // almost :)
    })

    Next steps

    Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/operations/actions.html b/docs/0.11.8/data-model/operations/actions.html index cec8972064..ecab1d41a0 100644 --- a/docs/0.11.8/data-model/operations/actions.html +++ b/docs/0.11.8/data-model/operations/actions.html @@ -19,8 +19,8 @@ - - + +
    @@ -42,7 +42,7 @@ Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

    1. args (type depends on the Action)

      An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

    2. context (type depends on the Action)

      An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

    Example

    The following Action:

    action createFoo {
    fn: import { createFoo } from "@server/actions.js"
    entities: [Foo]
    }

    Expects to find a named export createfoo from the file src/server/actions.js

    actions.js
    export const createFoo = (args, context) => {
    // implementation
    }

    The useAction Hook and Optimistic Updates

    Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

    When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

    The useAction hook accepts two arguments:

    • actionFn required

      The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

    • actionOptions

      An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

      • optimisticUpdates

        An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

        • getQuerySpecifier required

        A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).

        • updateQuery required

        The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

        • item - The argument you pass into the decorated Action.
        • oldData - The currently cached value for the Query identified by the specifier.
    caution

    The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

    Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

    Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

    Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

    src/client/pages/Task.jsx
    import React from 'react'
    import { useQuery } from '@wasp/queries'
    import { useAction } from '@wasp/actions'
    import getTask from '@wasp/queries/getTask'
    import markTaskAsDone from '@wasp/actions/markTaskAsDone'

    const TaskPage = ({ id }) => {
    const { data: task } = useQuery(getTask, { id })
    const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
    optimisticUpdates: [
    {
    getQuerySpecifier: ({ id }) => [getTask, { id }],
    updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
    },
    ],
    })

    if (!task) {
    return <h1>"Loading"</h1>
    }

    const { description, isDone } = task
    return (
    <div>
    <p>
    <strong>Description: </strong>
    {description}
    </p>
    <p>
    <strong>Is done: </strong>
    {isDone ? 'Yes' : 'No'}
    </p>
    {isDone || (
    <button onClick={() => markTaskAsDoneOptimistically({ id })}>
    Mark as done.
    </button>
    )}
    </div>
    )
    }

    export default TaskPage

    Advanced usage

    The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

    Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

    If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

    import getTasks from '@wasp/queries/getTasks'

    const queryKey = getTasks.queryCacheKey
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/operations/overview.html b/docs/0.11.8/data-model/operations/overview.html index f16f7eacd9..dde73fb826 100644 --- a/docs/0.11.8/data-model/operations/overview.html +++ b/docs/0.11.8/data-model/operations/overview.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.11.8

    Overview

    While Entities enable help you define your app's data model and relationships, Operations are all about working with this data.

    There are two kinds of Operations: Queries and Actions. As their names suggest, Queries are meant for reading data, and Actions are meant for changing it (either by updating existing entries or creating new ones).

    Keep reading to find out all there is to know about Operations in Wasp.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/data-model/operations/queries.html b/docs/0.11.8/data-model/operations/queries.html index 8f4c3fcfb1..e1ea8a774a 100644 --- a/docs/0.11.8/data-model/operations/queries.html +++ b/docs/0.11.8/data-model/operations/queries.html @@ -19,8 +19,8 @@ - - + +
    @@ -46,7 +46,7 @@ behavior for this particular Query. If you want to change the global defaults, you can do so in the client setup function.

    For an example of usage, check this section.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/editor-setup.html b/docs/0.11.8/editor-setup.html index ee2c655980..9330346d07 100644 --- a/docs/0.11.8/editor-setup.html +++ b/docs/0.11.8/editor-setup.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/general/cli.html b/docs/0.11.8/general/cli.html index 776dd4e550..d452343782 100644 --- a/docs/0.11.8/general/cli.html +++ b/docs/0.11.8/general/cli.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code and other cached artifacts.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about current Wasp project.
    test Executes tests in your project.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      [2] saas
      [3] todo-ts
      ▸ 1

      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      Deleting .wasp/ directory...
      Deleted .wasp/ directory.
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.11.1
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/general/language.html b/docs/0.11.8/general/language.html index 08195fbda1..f877a46e58 100644 --- a/docs/0.11.8/general/language.html +++ b/docs/0.11.8/general/language.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    Wasp Language (.wasp)

    Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

    It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

    It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

    Declarations

    The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

    app MyApp {
    title: "My app"
    }

    route RootRoute { path: "/", to: DashboardPage }

    page DashboardPage {
    component: import Dashboard from "@client/Dashboard.js"
    }

    In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

    Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

    • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
    • <declaration_name> is an identifier chosen by you to name this specific declaration
    • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

    So, for app declaration above, we have:

    • declaration type app
    • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
    • declaration body { title: "My app" }, which is a dictionary with field title that has string value. Type of this dictionary is in line with the declaration body type of the app declaration type. If we provided something else, e.g. changed title to little, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for app.

    Each declaration has a meaning behind it that describes how your web app should behave and function.

    All the other types in Wasp language (primitive types (string, number), composite types (dict, list), enum types (DbSystem), ...) are used to define the declaration bodies.

    Complete List of Wasp Types

    Wasp's type system can be divided into two main categories of types: fundamental types and domain types.

    While fundamental types are here to be basic building blocks of a a language, and are very similar to what you would see in other popular languages, domain types are what makes Wasp special, as they model the concepts of a web app like page, route and similar.

    • Fundamental types (source of truth)
      • Primitive types
        • string ("foo", "they said: \"hi\"")
        • bool (true, false)
        • number (12, 14.5)
        • declaration reference (name of existing declaration: TaskPage, updateTask)
        • ServerImport (external server import) (import Foo from "@server/bar.js", import { Smth } from "@server/a/b.js")
          • The path has to start with "@server". The rest is relative to the src/server directory.
          • import has to be a default import import Foo or a single named import import { Foo }.
        • ClientImport (external client import) (import Foo from "@client/bar.js", import { Smth } from "@client/a/b.js")
          • The path has to start with "@client". The rest is relative to the src/client directory.
          • import has to be a default import import Foo or a single named import import { Foo }.
        • json ({=json { a: 5, b: ["hi"] } json=})
        • psl (Prisma Schema Language) ({=psl <psl data model syntax> psl=})
      • Composite types
        • dict (dictionary) ({ a: 5, b: "foo" })
        • list ([1, 2, 3])
        • tuple ((1, "bar"), (2, 4, true))
          • Tuples can be of size 2, 3 and 4.
    • Domain types (source of truth)
      • Declaration types
        • action
        • api
        • apiNamespace
        • app
        • entity
        • job
        • page
        • query
        • route
        • crud
      • Enum types
        • DbSystem
        • HttpMethod
        • JobExecutor
        • EmailProvider

    You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/client-config.html b/docs/0.11.8/project/client-config.html index 3bf9376401..267c17e2b2 100644 --- a/docs/0.11.8/project/client-config.html +++ b/docs/0.11.8/project/client-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -35,7 +35,7 @@ renders a custom layout:

    src/client/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return (
    <Provider store={store}>
    <Layout>{children}</Layout>
    </Provider>
    )
    }

    function Layout({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }
  • setupFn: ClientImport

    You can use this function to perform any custom setup (e.g., setting up client-side periodic jobs).

    src/client/myClientSetupCode.js
    export default async function mySetupFunction() {
    // Run some code
    }
  • baseDir: String

    If you need to serve the client from a subdirectory, you can use the baseDir option.

    If you set baseDir to /my-app for example, that will make Wasp set the basename prop of the Router to /my-app. It will also set the base option of the Vite config to /my-app.

    This means that if you serve your app from https://example.com/my-app, the router will work correctly, and all the assets will be served from https://example.com/my-app.

    Setting the correct env variable

    If you set the baseDir option, make sure that the WASP_WEB_CLIENT_URL env variable also includes that base directory.

    For example, if you are serving your app from https://example.com/my-app, the WASP_WEB_CLIENT_URL should be also set to https://example.com/my-app, and not just https://example.com.

  • - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/css-frameworks.html b/docs/0.11.8/project/css-frameworks.html index de16c73d1f..c00f5e96a4 100644 --- a/docs/0.11.8/project/css-frameworks.html +++ b/docs/0.11.8/project/css-frameworks.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── src
    │   ├── client
    │   │   ├── tsconfig.json
    │   │   ├── Main.css
    │   │   ├── MainPage.js
    │   │   └── waspLogo.png
    │   ├── server
    │   │   └── tsconfig.json
    │   └── shared
    │   └── tsconfig.json
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [ "./src/**/*.{js,jsx,ts,tsx}" ],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/client/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/client/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, add it to dependencies in your main.wasp file and to the plugins list in your tailwind.config.cjs file:

    ./main.wasp
    app todoApp {
    // ...
    dependencies: [
    ("@tailwindcss/forms", "^0.5.3"),
    ("@tailwindcss/typography", "^0.5.7"),
    ],
    // ...
    }
    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/custom-vite-config.html b/docs/0.11.8/project/custom-vite-config.html index cf4191a95a..d592439ebf 100644 --- a/docs/0.11.8/project/custom-vite-config.html +++ b/docs/0.11.8/project/custom-vite-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Custom Vite Config

    Wasp uses Vite for serving the client during development and bundling it for production. If you want to customize the Vite config, you can do that by editing the vite.config.ts file in your src/client directory.

    Wasp will use your config and merge it with the default Wasp's Vite config.

    Vite config customization can be useful for things like:

    • Adding custom Vite plugins.
    • Customising the dev server.
    • Customising the build process.

    Be careful with making changes to the Vite config, as it can break the Wasp's client build process. Check out the default Vite config here to see what you can change.

    Examples

    Below are some examples of how you can customize the Vite config.

    Changing the Dev Server Behaviour

    If you want to stop Vite from opening the browser automatically when you run wasp start, you can do that by customizing the open option.

    src/client/vite.config.js
    export default {
    server: {
    open: false,
    },
    }

    Custom Dev Server Port

    You have access to all of the Vite dev server options in your custom Vite config. You can change the dev server port by setting the port option.

    src/client/vite.config.js
    export default {
    server: {
    port: 4000,
    },
    }
    .env.server
    WASP_WEB_CLIENT_URL=http://localhost:4000
    Changing the dev server port

    ⚠️ Be careful when changing the dev server port, you'll need to update the WASP_WEB_CLIENT_URL env var in your .env.server file.

    Customising the Base Path

    If you, for example, want to serve the client from a different path than /, you can do that by customizing the base option.

    src/client/vite.config.js
    export default {
    base: '/my-app/',
    }
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/customizing-app.html b/docs/0.11.8/project/customizing-app.html index f02ef21daa..ea432bd21c 100644 --- a/docs/0.11.8/project/customizing-app.html +++ b/docs/0.11.8/project/customizing-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.11.8

    Customizing the App

    Each Wasp project can have only one app type declaration. It is used to configure your app and its components.

    app todoApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ]
    }

    We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the API Reference.

    Changing the App Title

    You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the title field of your app declaration:

    app myApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "BookFace"
    }

    Adding Additional Lines to the Head

    If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the head field of your app declaration.

    An example of adding extra style sheets and scripts:

    app myApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "My App",
    head: [ // optional
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
    "<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
    ]
    }

    API Reference

    app todoApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ],
    auth: {
    // ...
    },
    client: {
    // ...
    },
    server: {
    // ...
    },
    db: {
    // ...
    },
    dependencies: [
    // ...
    ],
    emailSender: {
    // ...
    },
    webSocket: {
    // ...
    }
    }

    The app declaration has the following fields:

    • wasp: dict required Wasp compiler configuration. It is a dictionary with a single field:

      • version: string required

        The version specifies which versions of Wasp are compatible with the app. It should contain a valid SemVer range

        info

        For now, the version field only supports caret ranges (i.e., ^x.y.z). Support for the full specification will come in a future version of Wasp

    • title: string required

      Title of your app. It will appear in the browser tab, next to the favicon.

    • head: [string]

      List of additional lines (e.g. <link> or <script> tags) to be included in the <head> of your HTML document.

    The rest of the fields are covered in dedicated sections of the docs:

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/dependencies.html b/docs/0.11.8/project/dependencies.html index e22c0971b6..3896d168b5 100644 --- a/docs/0.11.8/project/dependencies.html +++ b/docs/0.11.8/project/dependencies.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    Dependencies

    Specifying npm dependencies in Wasp project is done via the dependencies field in the app declaration, in the following way:

    app MyApp {
    title: "My app",
    // ...
    dependencies: [
    ("redux", "^4.0.5"),
    ("react-redux", "^7.1.3")
    ]
    }

    You will need to re-run wasp start after adding a dependency for Wasp to pick it up.

    The quickest way to find out the latest version of a package is to run:

    npm view <package-name> version
    Using Packages that are Already Used by Wasp Internally

    In the current implementation of Wasp, if Wasp is already internally using a certain npm dependency with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version. If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp dictates exact versions of certain packages, so for example you can't choose version of React you want to use.

    We are currently working on a restructuring that will solve this and some other quirks that the current dependency system has: check issue #734 to follow our progress.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/env-vars.html b/docs/0.11.8/project/env-vars.html index 200da06143..1c8bbb889b 100644 --- a/docs/0.11.8/project/env-vars.html +++ b/docs/0.11.8/project/env-vars.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Env Variables

    Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.

    For instance, during development, you may want your project to connect to a local development database running on your machine, but in production, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.

    While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.

    In Wasp, you can use environment variables in both the client and the server code.

    Client Env Vars

    Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    To enable Wasp to pick them up, client environment variables must be prefixed with REACT_APP_, for example: REACT_APP_SOME_VAR_NAME=....

    You can read them from the client code like this:

    src/App.js
    console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)

    Check below on how to define them.

    Server Env Vars

    In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as SOME_VAR_NAME=....

    You can read them in the server code like this:

    console.log(process.env.SOME_VAR_NAME)

    Check below on how to define them.

    Defining Env Vars in Development

    During development, there are two ways to provide env vars to your Wasp project:

    1. Using .env files. (recommended)
    2. Using shell. (useful for overrides)

    1. Using .env (dotenv) Files

    Env vars usage in development

    This is the recommended method for providing env vars to your Wasp project during development.

    In the root of your Wasp project you can create two distinct files:

    • .env.server for env vars that will be provided to the server.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.server
      DATABASE_URL=postgresql://localhost:5432
      SOME_VAR_NAME=somevalue
    • .env.client for env vars that will be provided to the client.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.client
      REACT_APP_SOME_VAR_NAME=somevalue

    These files should not be committed to version control, and they are already ignored by default in the .gitignore file that comes with Wasp.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, or by defining them at the start of a command:

    SOME_VAR_NAME=SOMEVALUE wasp start

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    You should provide them to the build command, for example:

    REACT_APP_SOME_VAR_NAME=somevalue npm run build
    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME with the value you provided. This is done during the build process, so the value is embedded into the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/server-config.html b/docs/0.11.8/project/server-config.html index 96a8b562b2..431d913f36 100644 --- a/docs/0.11.8/project/server-config.html +++ b/docs/0.11.8/project/server-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/server/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/server/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/server/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ServerImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server routes or for example, setting up socket.io.

      src/server/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ServerImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/starter-templates.html b/docs/0.11.8/project/starter-templates.html index 3142e0ccb4..b410efd59d 100644 --- a/docs/0.11.8/project/starter-templates.html +++ b/docs/0.11.8/project/starter-templates.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    [2] saas
    [3] todo-ts
    ▸ 1

    🐝 --- Creating your project from the basic template... ---------------------------

    Created new Wasp app in ./MyFirstProject directory!
    To run it, do:

    cd MyFirstProject
    wasp start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: w/ Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    SaaS Template

    SaaS Template

    A SaaS Template to get your profitable side project started quickly and easily!

    Features: w/ Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Todo App w/ Typescript

    A simple Todo App with Typescript and Fullstack Type Safety.

    Features: Auth (username/password), Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/static-assets.html b/docs/0.11.8/project/static-assets.html index 20257f7a11..60d1fa8195 100644 --- a/docs/0.11.8/project/static-assets.html +++ b/docs/0.11.8/project/static-assets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Static Asset Handling

    Importing Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/client/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in a special public directory in the client folder:

    src
    └── client
    ├── public
    │ ├── favicon.ico
    │ └── robots.txt
    └── ...

    Assets in this directory will be served at root path / during dev, and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path - for example, src/client/public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/project/testing.html b/docs/0.11.8/project/testing.html index a13ad63c2c..e932e3d2a9 100644 --- a/docs/0.11.8/project/testing.html +++ b/docs/0.11.8/project/testing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src/client directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a realtime UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "@wasp/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "@wasp/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import getTasks from "@wasp/queries/getTasks";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "@wasp/types";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/client/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/client/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/client/Todo.jsx
    import { useQuery } from "@wasp/queries";
    import getTasks from "@wasp/queries/getTasks";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import getTasks from "@wasp/queries/getTasks";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/client/Todo.jsx
    import api from "@wasp/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    - - + + \ No newline at end of file diff --git a/docs/0.11.8/quick-start.html b/docs/0.11.8/quick-start.html index cff84a097a..b73db6d79c 100644 --- a/docs/0.11.8/quick-start.html +++ b/docs/0.11.8/quick-start.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.11.8

    Quick Start

    Installation

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser without any setup by running our Wasp Template for Gitpod

    Welcome, new Waspeteer 🐝!

    To install Wasp on Linux / OSX / WSL(Win), open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh

    ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    Then, create a new app by running:

    wasp new

    and then run the app:

    cd <my-project-name>
    wasp start

    That's it 🎉 You have successfully created and served a new web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong, or if you have additional questions.

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. We rely on the latest Node.js LTS version (currently v18.14.2).

    We recommend using nvm for managing your Node.js installation version(s).

    Quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 18

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 18

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh
    Running Wasp on Mac with Mx chip (arm64)

    Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)? Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install Rosetta on your Mac if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal

    softwareupdate --install-rosetta

    Once Rosetta is installed, you should be able to run Wasp without any issues.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/telemetry.html b/docs/0.11.8/telemetry.html index 5f048da72d..a0933dad9b 100644 --- a/docs/0.11.8/telemetry.html +++ b/docs/0.11.8/telemetry.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    Telemetry

    Overview

    The term telemetry refers to the collection of certain usage data to help improve the quality of a piece of software (in this case, Wasp).

    Our telemetry implementation is anonymized and very limited in its scope, focused on answering following questions:

    • How many people and how often: tried to install Wasp, use Wasp, have built a Wasp app, or have deployed one?
    • How many projects are created with Wasp?

    When and what is sent?

    • Information is sent via HTTPS request when wasp CLI command is invoked. Information is sent no more than twice in a period of 12 hours (sending is paused for 12 hours after last invocation, separately for wasp build command and for all other commands). Exact information as it is sent:
      {
      // Randomly generated, non-identifiable UUID representing a user.
      "distinct_id": "bf3fa7a8-1c11-4f82-9542-ec1a2d28786b",
      // Non-identifiable hash representing a project.
      "project_hash": "6d7e561d62b955d1",
      // True if command was `wasp build`, false otherwise.
      "is_build": true,
      // Captures `wasp deploy ...` args, but only those from the limited, pre-defined list of keywords.
      // Those are "fly", "setup", "create-db", "deploy" and "cmd". Everything else is ommited.
      "deploy_cmd_args": "fly;deploy",
      "wasp_version": "0.1.9.1",
      "os": "linux",
      // "CI" if running on CI, and whatever is the content of "WASP_TELEMETRY_CONTEXT" env var.
      // We use this to track when execution is happening in some special context, like on Gitpod, Replit or similar.
      "context": "CI"
      }
    • Information is also sent once via HTTPS request when wasp is installed via install.sh script. Exact information as it is sent:
      {
      // Randomly generated id.
      "distinct_id": "274701613078193779564259",
      "os": "linux"
      }

    Opting out

    You sharing the telemetry data with us means a lot to us, since it helps us understand how popular Wasp is, how it is being used, how the changes we are doing affect usage, how many new vs old users there are, and just in general how Wasp is doing. We look at these numbers every morning and they drive us to make Wasp better.

    However, if you wish to opt-out of telemetry, we understand! You can do so by setting the WASP_TELEMETRY_DISABLE environment variable to any value, e.g.:

    export WASP_TELEMETRY_DISABLE=1

    Future plans

    We don't have this implemented yet, but the next step will be to make telemetry go in two directions -> instead of just sending usage data to us, it will also at the same time check for any messages from our side (e.g. notification about new version of Wasp, or a security notice). Link to corresponding github issue.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/actions.html b/docs/0.11.8/tutorial/actions.html index e0a7c09c87..3fb5e40d74 100644 --- a/docs/0.11.8/tutorial/actions.html +++ b/docs/0.11.8/tutorial/actions.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    6. Modifying Data

    In the previous section, we learned about using queries to fetch data and only briefly mentioned that actions can be used to update the database. Let's learn more about actions so we can add and update tasks in the database.

    We have to create:

    1. A Wasp action that creates a new task.
    2. A React form that calls that action when the user creates a task.

    Creating a New Action

    Creating an action is very similar to creating a query.

    Declaring an Action

    We must first declare the action in main.wasp:

    main.wasp
    // ...

    action createTask {
    fn: import { createTask } from "@server/actions.js",
    entities: [Task]
    }

    Implementing an Action

    Let's now define a function for our createTask action:

    src/server/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/server/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src/server.

    Invoking the Action on the Client

    First, let's define a form that the user can create new tasks with.

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    // ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike queries, you call actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.

    Now, we just need to add this form to the page component:

    src/client/MainPage.tsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    And now we have a form that creates new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser, you'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! These automatic updates are handled by code that Wasp generates.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp will make sure that all your queries are kept in-sync with changes made by any actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done! We'll create a new action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an action named updateTask that takes a task id and an isDone in its arguments. You can check our implementation below.

    Solution

    The action declaration:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@server/actions.js",
    entities: [Task]
    }

    The implementation on the server:

    src/server/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    Now, we can call updateTask from the React component:

    src/client/MainPage.jsx
    // ...
    import updateTask from '@wasp/actions/updateTask'

    // ...

    const Task = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error) {
    window.alert('Error while updating task: ' + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ...

    Awesome! Now we can check off this task 🙃 Let's add one more interesting feature to our app.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/auth.html b/docs/0.11.8/tutorial/auth.html index 7260063d12..811b85ba91 100644 --- a/docs/0.11.8/tutorial/auth.html +++ b/docs/0.11.8/tutorial/auth.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    7. Adding Authentication

    Most apps today require some sort of registration and login flow, so Wasp has first-class support for it. Let's add it to our Todo app!

    First, we'll create a Todo list for what needs to be done (luckily we have an app for this now 😄).

    • Create a User entity.
    • Tell Wasp to use the username and password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify our queries and actions so that users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it expects certain fields to exist on the User entity. Specifically, it expects a unique username field and a password field, both of which should be strings.

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    psl=}

    As we talked about earlier, we have to remember to update the database schema:

    wasp db migrate-dev

    Adding Auth to the Project

    Next, we want to tell Wasp that we want to use full-stack authentication in our app:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app",

    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used a bit later.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import Signup from "@client/SignupPage.jsx"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import Login from "@client/LoginPage.jsx"
    }

    Great, Wasp now knows these pages exist! Now, the React code for the pages:

    src/client/LoginPage.jsx
    import { Link } from 'react-router-dom'
    import { LoginForm } from '@wasp/auth/forms/Login'

    const LoginPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <LoginForm />
    <br />
    <span>
    I don't have an account yet (<Link to="/signup">go to signup</Link>).
    </span>
    </div>
    )
    }

    export default LoginPage

    The Signup page is very similar to the login one:

    src/client/SignupPage.jsx
    import { Link } from 'react-router-dom'
    import { SignupForm } from '@wasp/auth/forms/Signup'

    const SignupPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <SignupForm />
    <br />
    <span>
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </div>
    )
    }

    export default SignupPage

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import Main from "@client/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/client/MainPage.jsx
    const MainPage = ({ user }) => {
    // Do something with the user
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    We see there is a user and that its password is already hashed 🤯

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    tasks Task[]
    psl=}

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User? @relation(fields: [userId], references: [id])
    userId Int?
    psl=}

    // ...

    As always, we have to update the database:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database. This is not recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional. Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/server/queries.js
    import HttpError from '@wasp/core/HttpError.js'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
    })
    }
    src/server/actions.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/client/MainPage.jsx
    // ...
    import logout from '@wasp/auth/logout'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/create.html b/docs/0.11.8/tutorial/create.html index f52e380857..93715fd282 100644 --- a/docs/0.11.8/tutorial/create.html +++ b/docs/0.11.8/tutorial/create.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    See Wasp In Action

    Prefer videos? We have a YouTube tutorial whick walks you through building this Todo app step by step. Check it out here!.

    We've also set up an in-browser dev environment for you on Gitpod which allows you to view and edit the completed app with no installation required.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start
    note

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder plage:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/entities.html b/docs/0.11.8/tutorial/entities.html index 4a97c66bcf..7e193d6625 100644 --- a/docs/0.11.8/tutorial/entities.html +++ b/docs/0.11.8/tutorial/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:

    main.wasp
    // ...

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}
    note

    Wasp uses Prisma as a way to talk to the database. You define entities by defining Prisma models using the Prisma Schema Language (PSL) between the {=psl psl=} tags.

    Read more in the Entities section of the docs.

    To update the database schema to include this entity, stop the wasp start process, if its running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/pages.html b/docs/0.11.8/tutorial/pages.html index 9b8ea6471f..15a52e8654 100644 --- a/docs/0.11.8/tutorial/pages.html +++ b/docs/0.11.8/tutorial/pages.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the default export from src/client/MainPage.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/client/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    const MainPage = () => {
    // ...
    }
    export default MainPage

    Since Wasp uses React for the frontend, this is a normal functional React component. It also uses the CSS and logo image that are located next to it in the src/client folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    tip

    wasp start automatically picks up the changes you make and restarts the app, so keep it running in the background.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import Hello from "@client/HelloPage.jsx"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/client/HelloPage and pass the URL parameter the same way as in React Router:

    src/client/HelloPage.jsx
    const HelloPage = (props) => {
    return <div>Here's {props.match.params.name}!</div>
    }

    export default HelloPage

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Let's prepare for building the Todo app by cleaning up the project and removing files and code we won't need. Start by deleting Main.css, waspLogo.png, and HelloPage.tsx that we just created in the src/client/ folder.

    Since we deleted HelloPage.tsx, we also need to remember to remove the route and page declarations we wrote for it. Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import Main from "@client/MainPage.jsx"
    }

    Next, we'll remove most of the code from the MainPage component:

    src/client/MainPage.jsx
    const MainPage = () => {
    return <div>Hello world!</div>
    }

    export default MainPage

    At this point, the main page should look like this:

    Todo App - Hello World

    In the next section, we'll start implementing some features of the Todo app!

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/project-structure.html b/docs/0.11.8/tutorial/project-structure.html index fa3d1bfbd9..b1bc77d51e 100644 --- a/docs/0.11.8/tutorial/project-structure.html +++ b/docs/0.11.8/tutorial/project-structure.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.11.8

    2. Project Structure

    After creating a new Wasp project, you'll get a file structure that looks like this:

    .
    ├── .gitignore
    ├── main.wasp # Your Wasp code goes here.
    ├── src
    │   ├── client # Your client code (JS/CSS/HTML) goes here.
    │   │   ├── Main.css
    │   │   ├── MainPage.jsx
    │   │   ├── tsconfig.json
    │   │   ├── vite.config.ts
    │   │   ├── vite-env.d.ts
    │   │   └── waspLogo.png
    │   ├── server # Your server code (Node JS) goes here.
    │   │   └── tsconfig.json
    │   ├── shared # Your shared (runtime independent) code goes here.
    │   │   └── tsconfig.json
    │   └── .waspignore
    └── .wasproot

    By your code, we mean the "the code you write", as opposed to the code generated by Wasp. Wasp expects you to separate all of your code—which we call external code—into three folders to make it obvious how each file is executed:

    • src/client: Contains the code executed on the client, in the browser.
    • src/server: Contains the code executed on the server, with Node.
    • src/shared: Contains code that may be executed on both the client and server.

    Many of the other files (tsconfig.json, vite-env.d.ts, etc.) are used by your IDE to improve your development experience with tools like autocompletion, intellisense, and error reporting. The file vite.config.ts is used to configure Vite, Wasp's build tool of choice. We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in custom Vite config docs.

    TypeScript Support

    Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla 🍦 JavaScript and TypeScript.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's look a bit closer at main.wasp.

    main.wasp

    This file, written in our Wasp configuration language, defines your app and lets Wasp take care a ton of features to your app for you. The file contains several declarations which, together, describe all the components of your app.

    The default Wasp file generated via wasp new on the previous page looks like:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.6" // Pins the version of Wasp to use.
    },
    title: "Todo app" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that will be rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/tutorial/queries.html b/docs/0.11.8/tutorial/queries.html index 43e76a2dc9..851f871bd9 100644 --- a/docs/0.11.8/tutorial/queries.html +++ b/docs/0.11.8/tutorial/queries.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.11.8

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using queries and actions, collectively known as operations.

    Queries are used to read an entity, while actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a query.

    To list tasks we have to:

    1. Create a query that fetches tasks from the database.
    2. Update the MainPage.tsx to use that query and display the results.

    Defining the Query

    We'll create a new query called getTasks. We'll need to declare the query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // Use `@server` to import files inside the `src/server` folder.
    fn: import { getTasks } from "@server/queries.js",
    // Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
    // will automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/server/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'asc' },
    })
    }

    Query function parameters:

    • args: object, arguments the query is given by the caller.
    • context: object, information provided by Wasp.

    Since we declared in main.wasp that our query uses the Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the src/server folder.

    Invoking the Query On the Frontend

    While we implement queries on the server, Wasp generates client-side functions that automatically takes care of serialization, network calls, and chache invalidation, allowing you to call the server code like it's a regular function. This makes it easy for us to use the getTasks query we just created in our React component:

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const Task = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <Task task={task} key={idx} />
    ))}
    </div>
    )
    }

    export default MainPage

    Most of this code is regular React, the only exception being the special @wasp imports:

    We could have called the query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the query changes. Remember that Wasp automatically refreshes queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    - - + + \ No newline at end of file diff --git a/docs/0.11.8/vision.html b/docs/0.11.8/vision.html index 61d6d5af6b..0f7d7e6791 100644 --- a/docs/0.11.8/vision.html +++ b/docs/0.11.8/vision.html @@ -19,8 +19,8 @@ - - + +
    @@ -33,7 +33,7 @@ They can be used inline (mixed with Wasp code) or provided via external files.
  • Has hatches (escape mechanisms) that allow you to customize your web app in all the right places, but remain hidden until you need them.
  • Entity (data model) is a first-class citizen - defined via custom Wasp syntax and it integrates very closely with the rest of the features, serving as one of the central concepts around which everything is built.
  • Out of the box support for CRUD UI based on the Entities, to get you quickly going, but also customizable to some level.
  • "Smart" operations (queries and actions) that in most cases automatically figure out when to update, and if not it is easy to define custom logic to compensate for that. User worries about client-server gap as little as possible.
  • Support, directly in Wasp, for declaratively defining simple components and operations.
  • Besides Wasp as a programming language, there will also be a visual builder that generates/edits Wasp code, allowing non-developers to participate in development. Since Wasp is declarative, we imagine such builder to naturally follow from Wasp language.
  • Server side rendering, caching, packaging, security, ... -> all those are taken care of by Wasp. You tell Wasp what you want, and Wasp figures out how to do it.
  • As simple deployment to production/staging as it gets.
  • While it comes with the official implementation(s), Wasp language will not be coupled with the single implementation. Others can provide implementations that compile to different web app stacks.
  • - - + + \ No newline at end of file diff --git a/docs/0.11.8/writingguide.html b/docs/0.11.8/writingguide.html index 8c251a4aeb..d33996aba4 100644 --- a/docs/0.11.8/writingguide.html +++ b/docs/0.11.8/writingguide.html @@ -19,8 +19,8 @@ - - + +
    @@ -53,7 +53,7 @@ Many of our titles are currently in title-case, we should start phasing those out.
  • Use the Oxford comma (e.g., "a, b, and c" instead of "a, b and c"). Why the Oxford comma is important
  • Content and Communication

    • Excellence comes from iteration. First drafts are always bad, but writing them is a vital part of the process. It's extremely difficult to avoid the slow progression of Bad -> OK -> Good -> Great -> Inspiring -> Transcendent.
    • Only wait until something is "Good" before publishing. Vue's guide originally says: "The community will help you push it further down the chain." We don't yet have that luxury, as our community isn't large enough. Still, we can't afford to invest too much time into the docs, so "Good" will have to do for now.

    Processes

    • Ideally, you should write the docs before you implement the feature. This will help you see the feature from the user's perspective and better spot the API's deficiencies and improvement potential. If something is difficult to explain, it's most likely difficult to understand. If it is difficult to understand, there might be a better way of designing it.
    • Try not to get defensive when receiving feedback. Our writing can be very personal to us, but if we get upset with the people who help us improve it, they will either stop giving feedback or start limiting the kind of feedback they give.
    • Proofread your work before showing it to others (and use Grammarly). If you show someone work with many spelling/grammar mistakes, you'll get feedback about spelling grammar/mistakes instead of more valuable notes about whether the writing is achieving your goals.
    • When you ask people for feedback, tell reviewers:
      • What you're trying to do.
      • What your fears are.
      • Which balances you're trying to strike.
    • Do your best to come up with a good and straighforward way to say something. Again, this will help the reviewer focus on high-level issues instead of rephrasing your sentences.
    • Read and correct your text several times before submitting it (preferably with some time between the readings). This is similar to proofreading but has more to do with content and communication style than grammar. A time offset is beneficial because it removes the text from your short-term memory, helping you view it more objectively.
    • It's OK to ask AI to improve your text. Just make sure to check it and correct it. You should always sign off on the last version.
    • When someone reports a problem, there is almost always a problem, even if the solution they proposed isn't quite right. Keep asking follow-up questions to learn more.
    • People need to feel safe asking questions when contributing/reviewing content. Here's how you can do that:
      • Thank people for their contributions/reviews, even if you're feeling grumpy. For example:
        • "Great question!"
        • "Thanks for taking the time to explain. 🙂"
        • "This is actually intentional, but thanks for taking the time to contribute. 😊"
      • Listen to what people are saying and mirror if you're not sure you're understanding correctly. This can help validate people's feelings and experiences while also understanding if you're understanding them correctly.
      • Use a lot of positive and empathetic emojis. It's always better to seem a little strange than mean or impatient. This primarily applies to Wasp team members speaking to outside contributors. Since most of the core team knows each other pretty well, there's no need to go overboard with the emojis and pleasantries.
      • Kindly communicate rules/boundaries. If someone behaves in a way that's abusive/inappropriate, respond only with kindness and maturity, but also make it clear that this behavior is not acceptable and what will happen (according to the code of conduct) if they continue behaving poorly.
    • All docs must go through the review cycle, preferably with more than a single reviewer. Different people focus on different things. Some of us are great at coming up with examples, others easily come up with analogies and explain complex topics, some have a clear and concise writing style, etc. Therefore, try to get at least two or three people to review your document.

    Possible improvements

    • Some parts of our docs don't follow all the guidelines outlined in this document. There's no need to start fixing all the issues right away. We can slowly improve the docs as we edit them.
    • We've discussed having a git repo with all the example code in the docs. This should make copying, pasting, testing, and maintaining code snippets easier.
    - - + + \ No newline at end of file diff --git a/docs/0.12.0.html b/docs/0.12.0.html index 147f4d2667..ea8ac58180 100644 --- a/docs/0.12.0.html +++ b/docs/0.12.0.html @@ -19,8 +19,8 @@ - - + +
    @@ -30,7 +30,7 @@ which are in their essence a Node.js functions that execute on server and can, thanks to Wasp, very easily be called from the client.

    First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up":

    main.wasp
    // Queries have automatic cache invalidation and are type-safe.
    query getRecipes {
    fn: import { getRecipes } from "@src/recipe/operations.ts",
    entities: [Recipe],
    }

    // Actions are type-safe and can be used to perform side-effects.
    action addRecipe {
    fn: import { addRecipe } from "@src/recipe/operations.ts",
    entities: [Recipe],
    }

    ... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):

    src/recipe/operations.ts
    // Wasp generates the types for you.
    import { type GetRecipes } from "wasp/server/operations";
    import { type Recipe } from "wasp/entities";

    export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
    return context.entities.Recipe.findMany( // Prisma query
    { where: { user: { id: context.user.id } } }
    );
    };

    export const addRecipe ...

    Now we can very easily use these in our React components!

    For the end, let's create a home page of our app.

    First we define it in main.wasp:

    main.wasp
    ...

    route HomeRoute { path: "/", to: HomePage }
    page HomePage {
    component: import { HomePage } from "@src/pages/HomePage",
    authRequired: true // Will send user to /login if not authenticated.
    }

    and then implement it as a React component in JS/TS (that calls the Operations we previously defined):

    src/pages/HomePage.tsx
    import { useQuery, getRecipes } from "wasp/client/operations";
    import { type User } from "wasp/entities";

    export function HomePage({ user }: { user: User }) {
    // Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
    const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here!

    if (isLoading) {
    return <div>Loading...</div>;
    }

    return (
    <div>
    <h1>Recipes</h1>
    <ul>
    {recipes ? recipes.map((recipe) => (
    <li key={recipe.id}>
    <div>{recipe.title}</div>
    <div>{recipe.description}</div>
    </li>
    )) : 'No recipes defined yet!'}
    </ul>
    </div>
    );
    }

    And voila! We are listing all the recipes in our app 🎉

    This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the Todo app tutorial.

    note

    Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.

    When to use Wasp

    Wasp is addressing the same core problems that typical web app frameworks are addressing, and it in big part looks, swims and quacks like a web app framework.

    Best used for

    • building full-stack web apps (like e.g. Airbnb or Asana)
    • quickly starting a web app with industry best practices
    • to be used alongside modern web dev stack (currently supported React and Node)

    Avoid using Wasp for

    • building static/presentational websites
    • to be used as a no-code solution
    • to be a solve-it-all tool in a single language

    Wasp is a DSL

    note

    You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below.

    Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a simple programming language that understands your code and can do a lot of things for you.

    Wasp is a programming language, but a specific kind: it is specialized for a single purpose: building modern web applications. We call such languages DSLs (Domain Specific Language).

    Other examples of DSLs that are often used today are e.g. SQL for databases and HTML for web page layouts. The main advantage and reason why DSLs exist is that they need to do only one task (e.g. database queries) so they can do it well and provide the best possible experience for the developer.

    The same idea stands behind Wasp - a language that will allow developers to build modern web applications with 10x less code and less stack-specific knowledge.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/apis.html b/docs/0.12.0/advanced/apis.html index 749d8e1412..5f1c381527 100644 --- a/docs/0.12.0/advanced/apis.html +++ b/docs/0.12.0/advanced/apis.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    Custom HTTP API Endpoints

    In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

    How to Create an API

    APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

    To create a Wasp API, you must:

    1. Declare the API in Wasp using the api declaration
    2. Define the API's NodeJS implementation

    After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

    Declaring the API in Wasp

    First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

    main.wasp
    // ...

    api fooBar { // APIs and their implementations don't need to (but can) have the same name.
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar")
    }

    Read more about the supported fields in the API Reference.

    Defining the API's NodeJS Implementation

    After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

    1. req: Express Request object
    2. res: Express Response object
    3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
    src/apis.js
    export const fooBar = (req, res, context) => {
    res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
    res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
    };

    Using the API

    Using the API externally

    To use the API externally, you simply call the endpoint using the method and path you used.

    For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

    Using the API from the Client

    To use the API from your client, including with auth support, you can import the Axios wrapper from wasp/client/api and invoke a call. For example:

    src/pages/SomePage.jsx
    import React, { useEffect } from "react";
    import { api } from "wasp/client/api";

    async function fetchCustomRoute() {
    const res = await api.get("/foo/bar");
    console.log(res.data);
    }

    export const Foo = () => {
    useEffect(() => {
    fetchCustomRoute();
    }, []);

    return <>// ...</>;
    };

    Making Sure CORS Works

    APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

    You can do this by defining custom middleware for your APIs in the Wasp file.

    For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

    main.wasp
    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo"
    }

    And then in the implementation file:

    src/apis.js
    export const apiMiddleware = (config) => {
    return config;
    };

    We are returning the default middleware which enables CORS for all APIs under the /foo path.

    For more information about middleware configuration, please see: Middleware Configuration

    Using Entities in APIs

    In many cases, resources used in APIs will be Entities. To use an Entity in your API, add it to the api declaration in Wasp:

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    entities: [Task],
    httpRoute: (GET, "/foo/bar")
    }

    Wasp will inject the specified Entity into the APIs context argument, giving you access to the Entity's Prisma API:

    src/apis.js
    export const fooBar = (req, res, context) => {
    res.json({ count: await context.entities.Task.count() });
    };

    The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

    API Reference

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar"),
    entities: [Task],
    auth: true,
    middlewareConfigFn: import { apiMiddleware } from "@src/apis"
    }

    The api declaration has the following fields:

    • fn: ExtImport required

      The import statement of the APIs NodeJs implementation.

    • httpRoute: (HttpMethod, string) required

      The HTTP (method, path) pair, where the method can be one of:

      • ALL, GET, POST, PUT or DELETE
      • and path is an Express path string.
    • entities: [Entity]

      A list of entities you wish to use inside your API. You can read more about it here.

    • auth: bool

      If auth is enabled, this will default to true and provide a context.user object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to false.

    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function for this API. See more in middleware section of the docs.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/deployment/cli.html b/docs/0.12.0/advanced/deployment/cli.html index 7fee660c9f..1d6b5cdb5d 100644 --- a/docs/0.12.0/advanced/deployment/cli.html +++ b/docs/0.12.0/advanced/deployment/cli.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Deploying with the Wasp CLI

    Wasp CLI can deploy your full-stack application with only a single command. The command automates the manual deployment process and is the recommended way of deploying Wasp apps.

    Supported Providers

    Wasp supports automated deployment to the following providers:

    • Fly.io - they offer 5$ free credit each month
    • Railway (coming soon, track it here #1157)

    Fly.io

    Prerequisites

    Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.

    You can add the required credit card information on the account's billing page.

    Fly.io CLI

    You will need the flyctl CLI installed on your machine before you can deploy to Fly.io.

    Deploying

    Using the Wasp CLI, you can easily deploy a new app to Fly.io with just a single command:

    wasp deploy fly launch my-wasp-app mia
    Specifying Org

    If your account is a member of more than one organization on Fly.io, you will need to specify under which one you want to execute the command. To do that, provide an additional --org <org-slug> option. You can find out the names(slugs) of your organizations by running fly orgs list.

    Please do not CTRL-C or exit your terminal while the commands are running.

    Under the covers, this runs the equivalent of the following commands:

    wasp deploy fly setup my-wasp-app mia
    wasp deploy fly create-db mia
    wasp deploy fly deploy

    The commands above use the app basename my-wasp-app and deploy it to the Miami, Florida (US) region (called mia). Read more about Fly.io regions here.

    Unique Name

    Your app name must be unique across all of Fly or deployment will fail.

    The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:

    • my-wasp-app-client
    • my-wasp-app-server
    • my-wasp-app-db

    You'll notice that Wasp creates two new files in your project root directory:

    • fly-server.toml
    • fly-client.toml

    You should include these files in your version control so that you can deploy your app with a single command in the future.

    Using a Custom Domain For Your App

    Setting up a custom domain is a three-step process:

    1. You need to add your domain to your Fly client app. You can do this by running:
    wasp deploy fly cmd --context client certs create mycoolapp.com
    Use Your Domain

    Make sure to replace mycoolapp.com with your domain in all of the commands mentioned in this section.

    This command will output the instructions to add the DNS records to your domain. It will look something like this:

    You can direct traffic to mycoolapp.com by:

    1: Adding an A record to your DNS service which reads

    A @ 66.241.1XX.154

    You can validate your ownership of mycoolapp.com by:

    2: Adding an AAAA record to your DNS service which reads:

    AAAA @ 2a09:82XX:1::1:ff40
    1. You need to add the DNS records for your domain:

      This will depend on your domain provider, but it should be a matter of adding an A record for @ and an AAAA record for @ with the values provided by the previous command.

    2. You need to set your domain as the WASP_WEB_CLIENT_URL environment variable for your server app:

    wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com

    We need to do this to keep our CORS configuration up to date.

    That's it, your app should be available at https://mycoolapp.com! 🎉

    API Reference

    launch

    launch is a convenience command that runs setup, create-db, and deploy in sequence.

    wasp deploy fly launch <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    It gives you the same result as running the following commands:

    wasp deploy fly setup <app-name> <region>
    wasp deploy fly create-db <region>
    wasp deploy fly deploy

    Environment Variables

    If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the --server-secret option:

    wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>

    setup

    setup will create your client and server apps on Fly, and add some secrets, but does not deploy them.

    wasp deploy fly setup <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    After running setup, Wasp creates two new files in your project root directory: fly-server.toml and fly-client.toml. You should include these files in your version control.

    You can edit the fly-server.toml and fly-client.toml files to further configure your Fly deployments. Wasp will use the TOML files when you run deploy.

    If you want to maintain multiple apps, you can add the --fly-toml-dir <abs-path> option to point to different directories, like "dev" or "staging".

    Execute Only Once

    You should only run setup once per app. If you run it multiple times, it will create unnecessary apps on Fly.

    create-db

    create-db will create a new database for your app.

    wasp deploy fly create-db <region>

    It accepts the following arguments:

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    Execute Only Once

    You should only run create-db once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.

    deploy

    wasp deploy fly deploy

    deploy pushes your client and server live.

    Run this command whenever you want to update your deployed app with the latest changes:

    wasp deploy fly deploy

    cmd

    If want to run arbitrary Fly commands (e.g. flyctl secrets list for your server app), here's how to do it:

    wasp deploy fly cmd secrets list --context server

    Fly.io Regions

    Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.

    Read more on Fly regions here.

    You can find the list of all available Fly regions by running:

    flyctl platform regions

    Environment Variables

    If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the secrets set command:

    wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server

    Multiple Fly Organizations

    If you have multiple organizations, you can specify a --org option. For example:

    wasp deploy fly launch my-wasp-app mia --org hive

    Building Locally

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.

    If you want to build locally, supply the --build-locally option to wasp deploy fly launch or wasp deploy fly deploy.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/deployment/manually.html b/docs/0.12.0/advanced/deployment/manually.html index 3414099d0e..0447187524 100644 --- a/docs/0.12.0/advanced/deployment/manually.html +++ b/docs/0.12.0/advanced/deployment/manually.html @@ -19,8 +19,8 @@ - - + +
    @@ -39,7 +39,7 @@ Set it to a random string at least 32 characters long (you can use an online generator).

  • PORT

    The server's HTTP port number. This is where the server listens for requests (default: 3001).

  • Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    While these are the general instructions on deploying the server anywhere, we also have more detailed instructions for chosen providers below, so check that out for more guidance if you are deploying to one of those providers.

    3. Deploying the Web Client (frontend)

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    The command above will build the web client and put it in the build/ directory in the web-app directory.

    Since the app's frontend is just a bunch of static files, you can deploy it to any static hosting provider.

    4. Deploying the Database

    Any PostgreSQL database will do, as long as you provide the server with the correct DATABASE_URL env var and ensure that the database is accessible from the server.

    Different Providers

    We'll cover a few different deployment providers below:

    • Fly.io (server and database)
    • Netlify (client)
    • Railway (server, client and database)
    • Heroku (server and database)

    Fly.io (server and database)

    We will show how to deploy the server and provision a database for it on Fly.io.

    We automated this process for you

    If you want to do all of the work below with one command, you can use the Wasp CLI.

    Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command.

    Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the flyctl CLI.

    note

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.

    Additionally, fly is a symlink for flyctl on most systems and they can be used interchangeably.

    Make sure you are logged in with flyctl CLI. You can check if you are logged in with flyctl auth whoami, and if you are not, you can log in with flyctl auth login.

    Set Up a Fly.io App

    info

    You need to do this only once per Wasp app.

    Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    Next, run the launch command to set up a new app and create a fly.toml file:

    flyctl launch --remote-only

    This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.

    • Say yes to Would you like to set up a Postgresql database now? and select Development. Fly.io will set a DATABASE_URL for you.

    • Say no to Would you like to deploy now? (and to any additional questions).

      We still need to set up several environment variables.

    What if the database setup fails?

    If your attempts to initiate a new app fail for whatever reason, then you should run flyctl apps destroy <app-name> before trying again. Fly does not allow you to create multiple apps with the same name.

    What does it look like when your DB is deployed correctly?

    When your DB is deployed correctly, you'll see it in the Fly.io dashboard:

    image

    Next, let's copy the fly.toml file up to our Wasp project dir for safekeeping.

    cp fly.toml ../../

    Next, let's add a few more environment variables:

    flyctl secrets set PORT=8080
    flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
    flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
    note

    If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

    Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    If you want to make sure you've added your secrets correctly, run flyctl secrets list in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.

    Deploy to a Fly.io App

    While still in the .wasp/build/ directory, run:

    flyctl deploy --remote-only --config ../../fly.toml

    This will build and deploy the backend of your Wasp app on Fly.io to https://<app-name>.fly.dev 🤘🎸

    Now, if you haven't, you can deploy your frontend and add the client url by running flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_frontend>. We suggest using Netlify for your frontend, but you can use any static hosting provider.

    Additionally, some useful flyctl commands:

    flyctl logs
    flyctl secrets list
    flyctl ssh console

    Redeploying After Wasp Builds

    When you rebuild your Wasp app (with wasp build), it will remove your .wasp/build/ directory. In there, you may have a fly.toml from any prior Fly.io deployments.

    While we will improve this process in the future, in the meantime, you have a few options:

    1. Copy the fly.toml file to a versioned directory, like your Wasp project dir.

      From there, you can reference it in flyctl deploy --config <path> commands, like above.

    2. Backup the fly.toml file somewhere before running wasp build, and copy it into .wasp/build/ after.

      When the fly.toml file exists in .wasp/build/ dir, you do not need to specify the --config <path>.

    3. Run flyctl config save -a <app-name> to regenerate the fly.toml file from the remote state stored in Fly.io.

    Netlify (client)

    We'll show how to deploy the client on Netlify.

    Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and Netlify CLI installed to follow these instructions.

    Make sure you are logged in with Netlify CLI. You can check if you are logged in with netlify status, and if you are not, you can log in with netlify login.

    First, make sure you have built the Wasp app. We'll build the client web app next.

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    We can now deploy the client with:

    netlify deploy

    Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.

    The final step is to run:

    netlify deploy --prod

    That is it! Your client should be live at https://<app-name>.netlify.app

    note

    Make sure you set this URL as the WASP_WEB_CLIENT_URL environment variable in your server hosting environment (e.g., Fly.io or Heroku).

    Railway (server, client and database)

    We will show how to deploy the client, the server, and provision a database on Railway.

    Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their plans page for more info.

    Prerequisites

    To get started, follow these steps:

    1. Make sure your Wasp app is built by running wasp build in the project dir.

    2. Create a Railway account

      Free Tier

      Sign up with your GitHub account to be eligible for the free tier

    3. Install the Railway CLI

    4. Run railway login and a browser tab will open to authenticate you.

    Create New Project

    Let's create our Railway project:

    1. Go to your Railway dashboard, click on New Project, and select Provision PostgreSQL from the dropdown menu.
    2. Once it initializes, right-click on the New button in the top right corner and select Empty Service.
    3. Once it initializes, click on it, go to Settings > General and change the name to server
    4. Go ahead and create another empty service and name it client

    Changing the name

    Deploy Your App to Railway

    Setup Domains

    We'll need the domains for both the server and client services:

    1. Go to the server instance's Settings tab, and click Generate Domain.
    2. Do the same under the client's Settings.

    Copy the domains as we will need them later.

    Deploying the Server

    Let's deploy our server first:

    1. Move into your app's .wasp/build/ directory:

      cd .wasp/build
    2. Link your app build to your newly created Railway project:

      railway link
    3. Go into the Railway dashboard and set up the required env variables:

      Open the Settings and go to the Variables tab:

      • click Variable reference and select DATABASE_URL (it will populate it with the correct value)

      • add WASP_WEB_CLIENT_URL - enter the the client domain (e.g. https://client-production-XXXX.up.railway.app)

      • add JWT_SECRET - enter a random string at least 32 characters long (use an online generator)

        Using an external auth method?

        If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    4. Push and deploy the project:

    railway up

    Select server when prompted with Select Service.

    Railway will now locate the Dockerfile and deploy your server 👍

    Deploying the Client

    1. Next, change into your app's frontend build directory .wasp/build/web-app:

      cd web-app
    2. Create the production build, using the server domain as the REACT_APP_API_URL:

      npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
    3. Next, we want to link this specific frontend directory to our project as well:

      railway link
    4. We need to configure Railway's static hosting for our client.

      Setting Up Static Hosting

      Copy the build folder within the web-app directory to dist:

      cp -r build dist

      We'll need to create the following files:

      • Dockerfile with:

        Dockerfile
        FROM pierrezemb/gostatic
        CMD [ "-fallback", "index.html" ]
        COPY ./dist/ /srv/http/
      • .dockerignore with:

        .dockerignore
        node_modules/

      You'll need to repeat these steps each time you run wasp build as it will remove the .wasp/build/web-app directory.

      Here's a useful shell script to do the process

      If you want to automate the process, save the following as deploy_client.sh in the root of your project:

      deploy_client.sh
      #!/usr/bin/env bash

      if [ -z "$REACT_APP_API_URL" ]
      then
      echo "REACT_APP_API_URL is not set"
      exit 1
      fi

      wasp build
      cd .wasp/build/web-app

      npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build

      cp -r build dist

      dockerfile_contents=$(cat <<EOF
      FROM pierrezemb/gostatic
      CMD [ "-fallback", "index.html" ]
      COPY ./dist/ /srv/http/
      EOF
      )

      dockerignore_contents=$(cat <<EOF
      node_modules/
      EOF
      )

      echo "$dockerfile_contents" > Dockerfile
      echo "$dockerignore_contents" > .dockerignore

      railway up

      Make it executable with:

      chmod +x deploy_client.sh

      You can run it with:

      REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
    5. Set the PORT environment variable to 8043 under the Variables tab.

    6. Deploy the client and select client when prompted with Select Service:

    railway up

    Conclusion

    And now your Wasp should be deployed! 🐝 🚂 🚀

    Back in your Railway dashboard, click on your project and you should see your newly deployed services: Postgres, Server, and Client.

    Updates & Redeploying

    When you make updates and need to redeploy:

    • run wasp build to rebuild your app
    • run railway up in the .wasp/build directory (server)
    • repeat all the steps in the .wasp/build/web-app directory (client)

    Heroku (server and database)

    We will show how to deploy the server and provision a database for it on Heroku.

    note

    Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

    As such, we recommend using an alternative provider like Fly.io for your first apps.

    You will need Heroku account, heroku CLI and docker CLI installed to follow these instructions.

    Make sure you are logged in with heroku CLI. You can check if you are logged in with heroku whoami, and if you are not, you can log in with heroku login.

    Set Up a Heroku App

    info

    You need to do this only once per Wasp app.

    Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:

    heroku create <app-name>

    Unless you have an external Postgres database that you want to use, let's create a new database on Heroku and attach it to our app:

    heroku addons:create --app <app-name> heroku-postgresql:mini
    caution

    Heroku does not offer a free plan anymore and mini is their cheapest database instance - it costs $5/mo.

    Heroku will also set DATABASE_URL env var for us at this point. If you are using an external database, you will have to set it up yourself.

    The PORT env var will also be provided by Heroku, so the only two left to set are the JWT_SECRET and WASP_WEB_CLIENT_URL env vars:

    heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
    heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
    note

    If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

    Deploy to a Heroku App

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    assuming you were at the root of your Wasp project at that moment.

    Log in to Heroku Container Registry:

    heroku container:login

    Build the docker image and push it to Heroku:

    heroku container:push --app <app-name> web

    App is still not deployed at this point. This step might take some time, especially the very first time, since there are no cached docker layers.

    Note for Apple Silicon Users

    Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of heroku container:push, users instead should:

    docker buildx build --platform linux/amd64 -t <app-name> .
    docker tag <app-name> registry.heroku.com/<app-name>/web
    docker push registry.heroku.com/<app-name>/web

    You are now ready to proceed to the next step.

    Deploy the pushed image and restart the app:

    heroku container:release --app <app-name> web

    This is it, the backend is deployed at https://<app-name>-XXXX.herokuapp.com 🎉

    Find out the exact app URL with:

    heroku info --app <app-name>

    Additionally, you can check out the logs with:

    heroku logs --tail --app <app-name>
    Using pg-boss with Heroku

    If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.

    Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/deployment/overview.html b/docs/0.12.0/advanced/deployment/overview.html index ba491f8764..5234cf6a77 100644 --- a/docs/0.12.0/advanced/deployment/overview.html +++ b/docs/0.12.0/advanced/deployment/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It also runs any pending migrations.

    You can add extra steps to this multi-stage Dockerfile by creating your own Dockerfile in the project's root directory. If Wasp finds a Dockerfile in the project's root, it appends its contents at the bottom of the default multi-stage Dockerfile.

    Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

    A few things to keep in mind:

    • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
    • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
    • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

    Read more in the official Docker docs on multi-stage builds.

    To see what your project's (potentially combined) Dockerfile will look like, run:

    wasp dockerfile

    Join our Discord if you have any questions, or if you need more customization than this hook provides.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/email.html b/docs/0.12.0/advanced/email.html index 4ddfbbc0bb..3e91e1d195 100644 --- a/docs/0.12.0/advanced/email.html +++ b/docs/0.12.0/advanced/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Sending Emails

    With Wasp's email-sending feature, you can easily integrate email functionality into your web application.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    Choose from one of the providers:

    • Dummy (development only),
    • Mailgun,
    • SendGrid
    • or the good old SMTP.

    Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

    Sending Emails

    Before jumping into details about setting up various providers, let's see how easy it is to send emails.

    You import the emailSender that is provided by the wasp/server/email module and call the send method on it.

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    Read more about the send method in the API Reference.

    The send method returns an object with the status of the sent email. It varies depending on the provider you use.

    Providers

    We'll go over all of the available providers in the next section. For some of them, you'll need to set up some env variables. You can do that in the .env.server file.

    Using the Dummy Provider

    Dummy Provider is not for production use

    The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    To speed up development, Wasp offers a Dummy email sender that console.logs the emails in the console. Since it doesn't send emails for real, it doesn't require any setup.

    Set the provider to Dummy in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Dummy,
    }
    }

    Using the SMTP Provider

    First, set the provider to SMTP in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SMTP,
    }
    }

    Then, add the following env variables to your .env.server file.

    .env.server
    SMTP_HOST=
    SMTP_USERNAME=
    SMTP_PASSWORD=
    SMTP_PORT=

    Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

    Using the Mailgun Provider

    Set the provider to Mailgun in the main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Mailgun,
    }
    }

    Then, get the Mailgun API key and domain and add them to your .env.server file.

    Getting the API Key and Domain

    1. Go to Mailgun and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    4. Go to Domains and create a new domain.
    5. Copy the domain and add it to your .env.server file.
    .env.server
    MAILGUN_API_KEY=
    MAILGUN_DOMAIN=

    Using the SendGrid Provider

    Set the provider field to SendGrid in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SendGrid,
    }
    }

    Then, get the SendGrid API key and add it to your .env.server file.

    Getting the API Key

    1. Go to SendGrid and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    .env.server
    SENDGRID_API_KEY=

    API Reference

    emailSender dict

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    The emailSender dict has the following fields:

    • provider: Provider required

      The provider you want to use. Choose from Dummy, SMTP, Mailgun or SendGrid.

      Dummy Provider is not for production use

      The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    • defaultFrom: dict

      The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

    JavaScript API

    Using the emailSender in :

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    The send method accepts an object with the following fields:

    • from: object

      The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

      • name: string

        The name of the sender.

      • email: string

        The email address of the sender.

    • to: string required

      The recipient's email address.

    • subject: string required

      The subject of the email.

    • text: string required

      The text version of the email.

    • html: string required

      The HTML version of the email

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/jobs.html b/docs/0.12.0/advanced/jobs.html index e685f0cdbf..a1dc95c972 100644 --- a/docs/0.12.0/advanced/jobs.html +++ b/docs/0.12.0/advanced/jobs.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Recurring Jobs

    In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

    What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

    Wasp supports background jobs that can help you with this:

    • Jobs persist between server restarts,
    • Jobs can be retried if they fail,
    • Jobs can be delayed until a future time,
    • Jobs can have a recurring schedule.

    Using Jobs

    Job Definition and Usage

    Let's write an example Job that will print a message to the console and return a list of tasks from the database.

    1. Start by creating a Job declaration in your .wasp file:

      main.wasp
      job mySpecialJob {
      executor: PgBoss,
      perform: {
      fn: import { foo } from "@src/workers/bar"
      },
      entities: [Task],
      }
    2. After declaring the Job, implement its worker function:

      src/workers/bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
      The worker function

      The worker function must be an async function. The function's return value represents the Job's result.

      The worker function accepts two arguments:

      • args: The data passed into the job when it's submitted.
      • context: { entities }: The context object containing entities you put in the Job declaration.
    3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'

      const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

      // Or, if you'd prefer it to execute in the future, just add a .delay().
      // It takes a number of seconds, Date, or ISO date string.
      await mySpecialJob
      .delay(10)
      .submit({ name: "Johnny" })

    And that'is it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

    In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

    Recurring Jobs

    If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar"
    },
    schedule: {
    cron: "0 * * * *",
    args: {=json { "job": "args" } json=} // optional
    }
    }

    In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

    API Reference

    Declaring Jobs

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar",
    executorOptions: {
    pgBoss: {=json { "retryLimit": 1 } json=}
    }
    },
    schedule: {
    cron: "*/5 * * * *",
    args: {=json { "foo": "bar" } json=},
    executorOptions: {
    pgBoss: {=json { "retryLimit": 0 } json=}
    }
    },
    entities: [Task],
    }

    The Job declaration has the following fields:

    • executor: JobExecutor required

      Job executors

      Our jobs need job executors to handle the scheduling, monitoring, and execution.

      PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires your app.db.system to be PostgreSQL.

      We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

      info

      Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

      pg-boss details

      pg-boss provides many useful features, which can be found here.

      When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

      If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

      pg-boss considerations

      • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
        • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
      • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
        • If you remove a schedule from a job, you will need to do the above as well.
      • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
      • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
    • perform: dict required

      • fn: ExtImport required

        • An async function that performs the work. Since Wasp executes Jobs on the server, the import path must lead to a NodeJS file.
        • It receives the following arguments:
          • args: Input: The data passed to the job when it's submitted.
          • context: { entities: Entities }: The context object containing any declared entities.

        Here's an example of a perform.fn function:

        src/workers/bar.js
        export const foo = async ({ name }, context) => {
        console.log(`Hello ${name}!`)
        const tasks = await context.entities.Task.findMany({})
        return { tasks }
        }
      • executorOptions: dict

        Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

        • pgBoss: JSON

          See the docs for pg-boss.

    • schedule: dict

      • cron: string required

        A 5-placeholder format cron expression string. See rationale for minute-level precision here.

        If you need help building cron expressions, Check out Crontab guru.

      • args: JSON

        The arguments to pass to the perform.fn function when invoked.

      • executorOptions: dict

        Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

        • pgBoss: JSON

          See the docs for pg-boss.

    • entities: [Entity]

      A list of entities you wish to use inside your Job (similar to Queries and Actions).

    JavaScript API

    • Importing a Job:

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'
    • submit(jobArgs, executorOptions)

      • jobArgs: Input

      • executorOptions: object

        Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

      someAction.js
      const submittedJob = await mySpecialJob.submit({ job: "args" })
    • delay(startAfter)

      • startAfter: int | string | Date required

        Delaying the invocation of the job handler. The delay can be one of:

        • Integer: number of seconds to delay. [Default 0]
        • String: ISO date string to run at.
        • Date: Date to run at.
      someAction.js
      const submittedJob = await mySpecialJob
      .delay(10)
      .submit({ job: "args" }, { "retryLimit": 2 })

    Tracking

    The return value of submit() is an instance of SubmittedJob, which has the following fields:

    • jobId: The ID for the job in that executor.
    • jobName: The name of the job you used in your .wasp file.
    • executorName: The Symbol of the name of the job executor.

    There are also some namespaced, job executor-specific objects.

    • For pg-boss, you may access: pgBoss
      • details(): pg-boss specific job detail information. Reference
      • cancel(): attempts to cancel a job. Reference
      • resume(): attempts to resume a canceled job. Reference
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/links.html b/docs/0.12.0/advanced/links.html index 31bc7d0b12..164e0865f0 100644 --- a/docs/0.12.0/advanced/links.html +++ b/docs/0.12.0/advanced/links.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Type-Safe Links

    If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

    After you defined a route:

    main.wasp
    route TaskRoute { path: "/task/:id", to: TaskPage }
    page TaskPage { ... }

    You can get the benefits of type-safe links by using the Link component from wasp/client/router:

    TaskList.tsx
    import { Link } from 'wasp/client/router'

    export const TaskList = () => {
    // ...

    return (
    <div>
    {tasks.map((task) => (
    <Link
    key={task.id}
    to="/task/:id"
    {/* 👆 You must provide a valid path here */}
    params={{ id: task.id }}>
    {/* 👆 All the params must be correctly passed in */}
    {task.description}
    </Link>
    ))}
    </div>
    )
    }

    Using Search Query & Hash

    You can also pass search and hash props to the Link component:

    TaskList.tsx
    <Link
    to="/task/:id"
    params={{ id: task.id }}
    search={{ sortBy: 'date' }}
    hash="comments"
    >
    {task.description}
    </Link>

    This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

    The routes Object

    You can also get all the pages in your app with the routes object:

    TaskList.tsx
    import { routes } from 'wasp/client/router'

    const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

    This will result in a link like this: /task/1.

    You can also pass search and hash props to the build function. Check out the API Reference for more details.

    API Reference

    The Link component accepts the following props:

    • to required

      • A valid Wasp Route path from your main.wasp file.
    • params: { [name: string]: string | number } required (if the path contains params)

      • An object with keys and values for each param in the path.
      • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
    • search: string[][] | Record<string, string> | string | URLSearchParams

      • Any valid input for URLSearchParams constructor.
      • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
    • hash: string

    • all other props that the react-router-dom's Link component accepts

    routes Object

    The routes object contains a function for each route in your app.

    router.tsx
    export const routes = {
    // RootRoute has a path like "/"
    RootRoute: {
    build: (options?: {
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }) => // ...
    },

    // DetailRoute has a path like "/task/:id/:something?"
    DetailRoute: {
    build: (
    options: {
    params: { id: ParamValue; something?: ParamValue; },
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }
    ) => // ...
    }
    }

    The params object is required if the route contains params. The search and hash parameters are optional.

    You can use the routes object like this:

    import { routes } from 'wasp/client/router'

    const linkToRoot = routes.RootRoute.build()
    const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/middleware-config.html b/docs/0.12.0/advanced/middleware-config.html index 717449c654..e26d819b69 100644 --- a/docs/0.12.0/advanced/middleware-config.html +++ b/docs/0.12.0/advanced/middleware-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Configuring Middleware

    Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

    Default Global Middleware 🌍

    Wasp's Express server has the following middleware by default:

    • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

    • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

      note

      CORS middleware is required for the frontend to communicate with the backend.

    • Morgan: HTTP request logger middleware.

    • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

      note

      JSON middlware is required for Operations to function properly.

    • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

    • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

    Customization

    You have three places where you can customize middleware:

    1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

      Modifying global middleware

      Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

    2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

    3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

      • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

    Default Middleware Definitions

    Below is the actual definitions of default middleware which you can override.

    const defaultGlobalMiddleware = new Map([
    ['helmet', helmet()],
    ['cors', cors({ origin: config.allowedCORSOrigins })],
    ['logger', logger('dev')],
    ['express.json', express.json()],
    ['express.urlencoded', express.urlencoded({ extended: false })],
    ['cookieParser', cookieParser()]
    ])

    1. Customize Global Middleware

    If you would like to modify the middleware for all operations and APIs, you can do something like:

    main.wasp
    app todoApp {
    // ...

    server: {
    setupFn: import setup from "@src/serverSetup",
    middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"
    },
    }
    src/serverSetup.js
    import cors from 'cors'
    import { config } from 'wasp/server'

    export const serverMiddlewareFn = (middlewareConfig) => {
    // Example of adding extra domains to CORS.
    middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
    return middlewareConfig
    }

    2. Customize api-specific Middleware

    If you would like to modify the middleware for a single API, you can do something like:

    main.wasp
    // ...

    api webhookCallback {
    fn: import { webhookCallback } from "@src/apis",
    middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",
    httpRoute: (POST, "/webhook/callback"),
    auth: false
    }
    src/apis.js
    import express from 'express'

    export const webhookCallback = (req, res, _context) => {
    res.json({ msg: req.body.length })
    }

    export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
    console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

    middlewareConfig.delete('express.json')
    middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

    return middlewareConfig
    }

    note

    This gets installed on a per-method basis. Behind the scenes, this results in code like:

    router.post('/webhook/callback', webhookCallbackMiddleware, ...)

    3. Customize Per-Path Middleware

    If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

    main.wasp
    // ...

    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo/bar"
    }
    src/apis.js
    export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
    const customMiddleware = (_req, _res, next) => {
    console.log('fooBarNamespaceMiddlewareFn: custom middleware')
    next()
    }

    middlewareConfig.set('custom.middleware', customMiddleware)

    return middlewareConfig
    }
    note

    This gets installed at the router level for the path. Behind the scenes, this results in something like:

    router.use('/foo/bar', fooBarNamespaceMiddleware)
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/advanced/web-sockets.html b/docs/0.12.0/advanced/web-sockets.html index dbd415d313..e1b0939d6a 100644 --- a/docs/0.12.0/advanced/web-sockets.html +++ b/docs/0.12.0/advanced/web-sockets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Web Sockets

    Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

    We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

    To get started, you need to:

    1. Define your WebSocket logic on the server.
    2. Enable WebSockets in your Wasp file, and connect it with your server logic.
    3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
    4. Optionally, type the WebSocket events and payloads for full-stack type safety.

    Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

    Turn On WebSockets in Your Wasp File

    We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    Defining the Events Handler

    Let's define the WebSockets server with all of the events and handler functions.

    webSocketFn Function

    On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

    You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

    This is how we can define our webSocketFn function:

    src/webSocket.js
    import { v4 as uuidv4 } from 'uuid'
    import { getFirstProviderUserId } from 'wasp/auth'

    export const webSocketFn = (io, context) => {
    io.on('connection', (socket) => {
    const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
    console.log('a user connected: ', username)

    socket.on('chatMessage', async (msg) => {
    console.log('message: ', msg)
    io.emit('chatMessage', { id: uuidv4(), username, text: msg })
    // You can also use your entities here:
    // await context.entities.SomeEntity.create({ someField: msg })
    })
    })
    }

    Using the WebSocket On The Client

    useSocket Hook

    Client access to WebSockets is provided by the useSocket hook. It returns:

    • socket: Socket for sending and receiving events.
    • isConnected: boolean for showing a display of the Socket.IO connection status.
      • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
      • If you set autoConnect: false in your Wasp file, then you should call these as needed.

    All components using useSocket share the same underlying socket.

    useSocketListener Hook

    Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

    src/ChatPage.jsx
    import React, { useState } from 'react'
    import {
    useSocket,
    useSocketListener,
    } from 'wasp/client/webSocket'

    export const ChatPage = () => {
    const [messageText, setMessageText] = useState('')
    const [messages, setMessages] = useState([])
    const { socket, isConnected } = useSocket()

    useSocketListener('chatMessage', logMessage)

    function logMessage(msg) {
    setMessages((priorMessages) => [msg, ...priorMessages])
    }

    function handleSubmit(e) {
    e.preventDefault()
    socket.emit('chatMessage', messageText)
    setMessageText('')
    }

    const messageList = messages.map((msg) => (
    <li key={msg.id}>
    <em>{msg.username}</em>: {msg.text}
    </li>
    ))
    const connectionIcon = isConnected ? '🟢' : '🔴'

    return (
    <>
    <h2>Chat {connectionIcon}</h2>
    <div>
    <form onSubmit={handleSubmit}>
    <div>
    <div>
    <input
    type="text"
    value={messageText}
    onChange={(e) => setMessageText(e.target.value)}
    />
    </div>
    <div>
    <button type="submit">Submit</button>
    </div>
    </div>
    </form>
    <ul>{messageList}</ul>
    </div>
    </>
    )
    }

    API Reference

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    The webSocket dict has the following fields:

    • fn: WebSocketFn required

      The function that defines the WebSocket events and handlers.

    • autoConnect: bool

      Whether to automatically connect to the WebSocket server. Default: true.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/email.html b/docs/0.12.0/auth/email.html index 83b7b136f9..9e5c2f855a 100644 --- a/docs/0.12.0/auth/email.html +++ b/docs/0.12.0/auth/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Email

    Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

    Auth UI

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Setting Up Email Authentication

    We'll need to take the following steps to set up email authentication:

    1. Enable email authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages
    5. Set up the email sender

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }

    // Defining User entity
    entity User { ... }

    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Email Authentication in main.wasp

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable email authentication
    email: {
    // 3. Specify the email from field
    fromField: {
    name: "My App Postman",
    email: "hello@itsme.com"
    },
    // 4. Specify the email verification and password reset options (we'll talk about them later)
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    },
    },
    },
    onAuthFailedRedirectTo: "/login",
    onAuthSucceededRedirectTo: "/"
    },
    }

    Read more about the email auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    main.wasp
    // 5. Define the user entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    psl=}

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...

    // 6. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { RequestPasswordReset } from "@src/pages/auth.jsx",
    }

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { PasswordReset } from "@src/pages/auth.jsx",
    }

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { EmailVerification } from "@src/pages/auth.jsx",
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import {
    LoginForm,
    SignupForm,
    VerifyEmailForm,
    ForgotPasswordForm,
    ResetPasswordForm,
    } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    <br />
    <span className="text-sm font-medium text-gray-900">
    Forgot your password? <Link to="/request-password-reset">reset it</Link>
    .
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    export function EmailVerification() {
    return (
    <Layout>
    <VerifyEmailForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    export function RequestPasswordReset() {
    return (
    <Layout>
    <ForgotPasswordForm />
    </Layout>
    );
    }

    export function PasswordReset() {
    return (
    <Layout>
    <ResetPasswordForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    5. Set up an Email Sender

    To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

    We'll use the Dummy provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the supported email providers.

    To set up the Dummy provider to send emails, add the following to the main.wasp file:

    main.wasp
    app myApp {
    // ...
    // 7. Set up the email sender
    emailSender: {
    provider: Dummy,
    }
    }

    Conclusion

    That's it! We have set up email authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the auth overview.

    Login and Signup Flows

    Login

    Auth UI

    Signup

    Auth UI

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

    3. Allowing registration for unverified emails

      If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

    4. Password validation

      Read more about the default password validation rules and how to override them in auth overview docs.

    Email Verification Flow

    Automatic email verification in development

    In development mode, you can skip the email verification step by setting the SKIP_EMAIL_VERIFICATION_IN_DEV environment variable to true in your .env.server file:

    .env.server
    SKIP_EMAIL_VERIFICATION_IN_DEV=true

    This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app.

    By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

    Our setup looks like this:

    main.wasp
    // ...

    emailVerification: {
    clientRoute: EmailVerificationRoute,
    }

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

    The content of the e-mail can be customized, read more about it here.

    Email Verification Page

    We defined our email verification page in the auth.tsx file.

    Auth UI

    Password Reset Flow

    Users can request a password and then they'll receive an e-mail with a link to reset their password.

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

    Our setup in main.wasp looks like this:

    main.wasp
    // ...

    passwordReset: {
    clientRoute: PasswordResetRoute,
    }

    Request Password Reset Page

    Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

    Request password reset page

    Password Reset Page

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

    Request password reset page

    Users can enter their new password there.

    The content of the e-mail can be customized, read more about it here.

    Creating a Custom Sign-up Action

    Creating a custom sign-up action

    We don't recommend creating a custom sign-up action unless you have a good reason to do so. It is a complex process and you can easily make a mistake that will compromise the security of your app.

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidEmail,
    createProviderId,
    sanitizeAndSerializeProviderData,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    createUser,
    createEmailVerificationLink,
    sendEmailVerificationEmail,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidEmail(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('email', args.email)
    const existingAuthIdentity = await findAuthIdentity(providerId)

    if (existingAuthIdentity) {
    const providerData = deserializeAndSanitizeProviderData(existingAuthIdentity.providerData)
    // Your custom code here
    } else {
    // sanitizeAndSerializeProviderData will hash the user's password
    const newUserProviderData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    isEmailVerified: false,
    emailVerificationSentAt: null,
    passwordResetSentAt: null,
    })
    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )

    // Verification link links to a client route e.g. /email-verification
    const verificationLink = await createEmailVerificationLink(args.email, '/email-verification');
    try {
    await sendEmailVerificationEmail(
    args.email,
    {
    from: {
    name: "My App Postman",
    email: "hello@itsme.com",
    },
    to: args.email,
    subject: "Verify your email",
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    }
    );
    } catch (e: unknown) {
    console.error("Failed to send email verification email:", e);
    throw new HttpError(500, "Failed to send email verification email.");
    }
    }
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Email

    • ensureValidEmail(args)

      Checks if the email is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    getEmail

    If you are looking to access the user's email in your code, you can do that by accessing the info about the user that is stored in the user.auth.identities array.

    To make things a bit easier for you, Wasp offers the getEmail helper.

    The getEmail helper returns the user's email or null if the user doesn't have an email auth identity.

    src/MainPage.jsx
    import { getEmail } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const email = getEmail(user)
    // ...
    }
    src/tasks.js
    import { getEmail } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const email = getEmail(context.user)
    // ...
    }

    API Reference

    Let's go over the options we can specify when using email authentication.

    userEntity fields

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    // We'll explain these options below
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the email dict

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    userSignupFields: import { userSignupFields } from "@src/auth.js",
    fromField: {
    name: "My App",
    email: "hello@itsme.com"
    },
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js",
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js",
    },
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })
    Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields).

    fromField: EmailFromField required

    fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

    It has the following fields:

    • name: name of the sender
    • email: e-mail address of the sender required

    emailVerification: EmailVerificationConfig required

    emailVerification is a dict that specifies the details of the e-mail verification process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

      Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

      src/pages/EmailVerificationPage.jsx
      import { verifyEmail } from 'wasp/client/auth'
      ...
      await verifyEmail({ token });
      note

      We used Auth UI above to avoid doing this work of sending the token to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn can be done by defining a file in the src directory.

      src/email.js
      export const getVerificationEmailContent = ({ verificationLink }) => ({
      subject: 'Verify your email',
      text: `Click the link below to verify your email: ${verificationLink}`,
      html: `
      <p>Click the link below to verify your email</p>
      <a href="${verificationLink}">Verify email</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.

    passwordReset: PasswordResetConfig required

    passwordReset is a dict that specifies the password reset process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to reset their password. required

      Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

      src/pages/ForgotPasswordPage.jsx
      import { requestPasswordReset } from 'wasp/client/auth'
      ...
      await requestPasswordReset({ email });
      src/pages/PasswordResetPage.jsx
      import { resetPassword } from 'wasp/client/auth'
      ...
      await resetPassword({ password, token })
      note

      We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn is done by defining a function that looks like this:

      src/email.js
      export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
      subject: 'Password reset',
      text: `Click the link below to reset your password: ${passwordResetLink}`,
      html: `
      <p>Click the link below to reset your password</p>
      <a href="${passwordResetLink}">Reset password</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/entities.html b/docs/0.12.0/auth/entities.html index 5435701d43..bc2a28da7d 100644 --- a/docs/0.12.0/auth/entities.html +++ b/docs/0.12.0/auth/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Auth Entities

    Wasp supports multiple different authentication methods and for each method, we need to store different information about the user. For example, if you are using the Username & password authentication method, we need to store the user's username and password. On the other hand, if you are using the Email authentication method, you will need to store the user's email, password and for example, their email verification status.

    Entities Explained

    To store user information, Wasp creates a few entities behind the scenes. In this section, we will explain what entities are created and how they are connected.

    User Entity

    When you want to add authentication to your app, you need to specify the user entity e.g. User in your Wasp file. This entity is a "business logic user" which represents a user of your app.

    You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address. You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created.

    entity User {=psl
    id Int @id @default(autoincrement())
    // Any other fields you want to store about the user
    psl=}

    You own the user entity and you can modify it as you wish. You can add new fields to it, remove fields from it, or change the type of the fields. You can also add new relations to it or remove existing relations from it.

    Auth Entities in a Wasp App
    Auth Entities in a Wasp App

    On the other hand, the Auth, AuthIdentity and Session entities are created behind the scenes and are used to store the user's login credentials. You as the developer don't need to care about this entity most of the time. Wasp owns these entities.

    In the case you want to create a custom signup action, you will need to use the Auth and AuthIdentity entities directly.

    Example App Model

    Let's imagine we created a simple tasks management app:

    • The app has email and Google-based auth.
    • Users can create tasks and see the tasks that they have created.

    Let's look at how would that look in the database:

    Example of Auth Entities
    Example of Auth Entities

    If we take a look at an example user in the database, we can see:

    • The business logic user, User is connected to multiple Task entities.
      • In this example, "Example User" has two tasks.
    • The User is connected to exactly one Auth entity.
    • Each Auth entity can have multiple AuthIdentity entities.
      • In this example, the Auth entity has two AuthIdentity entities: one for the email-based auth and one for the Google-based auth.
    • Each Auth entity can have multiple Session entities.
      • In this example, the Auth entity has one Session entity.
    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Auth Entity internal

    Wasp's internal Auth entity is used to connect the business logic user, User with the user's login credentials.

    entity Auth {=psl
    id String @id @default(uuid())
    userId Int? @unique
    // Wasp injects this relation on the User entity as well
    user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
    identities AuthIdentity[]
    sessions Session[]
    psl=}

    The Auth fields:

    • id is a unique identifier of the Auth entity.
    • userId is a foreign key to the User entity.
      • It is used to connect the Auth entity with the business logic user.
    • user is a relation to the User entity.
      • This relation is injected on the User entity as well.
    • identities is a relation to the AuthIdentity entity.
    • sessions is a relation to the Session entity.

    AuthIdentity Entity internal

    The AuthIdentity entity is used to store the user's login credentials for various authentication methods.

    entity AuthIdentity {=psl
    providerName String
    providerUserId String
    providerData String @default("{}")
    authId String
    auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade)

    @@id([providerName, providerUserId])
    psl=}

    The AuthIdentity fields:

    • providerName is the name of the authentication provider.
      • For example, email or google.
    • providerUserId is the user's ID in the authentication provider.
      • For example, the user's email or Google ID.
    • providerData is a JSON string that contains additional data about the user from the authentication provider.
    • authId is a foreign key to the Auth entity.
      • It is used to connect the AuthIdentity entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Session Entity internal

    The Session entity is used to store the user's session information. It is used to keep the user logged in between page refreshes.

    entity Session {=psl
    id String @id @unique
    expiresAt DateTime
    userId String
    auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade)

    @@index([userId])
    psl=}

    The Session fields:

    • id is a unique identifier of the Session entity.
    • expiresAt is the date when the session expires.
    • userId is a foreign key to the Auth entity.
      • It is used to connect the Session entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Accessing the Auth Fields

    If you are looking to access the user's email or username in your code, you can do that by accessing the info about the user that is stored in the AuthIdentity entity.

    Everywhere where Wasp gives you the user object, it also includes the auth relation with the identities relation. This means that you can access the auth identity info by using the user.auth.identities array.

    To make things a bit easier for you, Wasp offers a few helper functions that you can use to access the auth identity info.

    getEmail

    The getEmail helper returns the user's email or null if the user doesn't have an email auth identity.

    src/MainPage.jsx
    import { getEmail } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const email = getEmail(user)
    // ...
    }
    src/tasks.js
    import { getEmail } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const email = getEmail(context.user)
    // ...
    }

    getUsername

    The getUsername helper returns the user's username or null if the user doesn't have a username auth identity.

    src/MainPage.jsx
    import { getUsername } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const username = getUsername(user)
    // ...
    }
    src/tasks.js
    import { getUsername } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const username = getUsername(context.user)
    // ...
    }

    getFirstProviderUserId

    The getFirstProviderUserId helper returns the first user ID (e.g. username or email) that it finds for the user or null if it doesn't find any.

    As mentioned before, the providerUserId field is how providers identify our users. For example, the user's username in the case of the username auth or the user's email in the case of the email auth. This can be useful if you support multiple authentication methods and you need any ID that identifies the user in your app.

    src/MainPage.jsx
    import { getFirstProviderUserId } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const userId = getFirstProviderUserId(user)
    // ...
    }
    src/tasks.js
    import { getFirstProviderUserId } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const userId = getFirstProviderUserId(context.user)
    // ...
    }

    findUserIdentity

    You can find a specific auth identity by using the findUserIdentity helper function. This function takes a user and a providerName and returns the first providerName identity that it finds or null if it doesn't find any.

    Possible provider names are:

    • email
    • username
    • google
    • github

    This can be useful if you want to check if the user has a specific auth identity. For example, you might want to check if the user has an email auth identity or Google auth identity.

    src/MainPage.jsx
    import { findUserIdentity } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const emailIdentity = findUserIdentity(user, 'email')
    const googleIdentity = findUserIdentity(user, 'google')
    if (emailIdentity) {
    // ...
    } else if (googleIdentity) {
    // ...
    }
    // ...
    }
    src/tasks.js
    import { findUserIdentity } from 'wasp/client/auth'

    export const createTask = async (args, context) => {
    const emailIdentity = findUserIdentity(context.user, 'email')
    const googleIdentity = findUserIdentity(context.user, 'google')
    if (emailIdentity) {
    // ...
    } else if (googleIdentity) {
    // ...
    }
    // ...
    }

    Custom Signup Action

    Let's take a look at how you can use the Auth and AuthIdentity entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service.

    Custom Signup Examples

    In the Email section of the docs we give you an example for custom email signup and in the Username & password section of the docs we give you an example for custom username & password signup.

    Below is a simplified version of a custom signup action which you probably wouldn't use in your app but it shows you how you can use the Auth and AuthIdentity entities to create a custom signup action.

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    entities: [User]
    }
    src/auth/signup.js
    import {
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, { entities: { User } }) => {
    try {
    // Provider ID is a combination of the provider name and the provider user ID
    // And it is used to uniquely identify the user in your app
    const providerId = createProviderId('username', args.username)
    // sanitizeAndSerializeProviderData hashes the password and returns a JSON string
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )

    // This is equivalent to:
    // await User.create({
    // data: {
    // auth: {
    // create: {
    // identities: {
    // create: {
    // providerName: 'username',
    // providerUserId: args.username
    // providerData,
    // },
    // },
    // }
    // },
    // }
    // })
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    You can use whichever method suits your needs better: either the createUser function or Prisma's User.create method. The createUser function is a bit more convenient to use because it hides some of the complexity. On the other hand, the User.create method gives you more control over the data that is stored in the Auth and AuthIdentity entities.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/overview.html b/docs/0.12.0/auth/overview.html index 22802ce804..7c2f6714f2 100644 --- a/docs/0.12.0/auth/overview.html +++ b/docs/0.12.0/auth/overview.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Overview

    Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box.

    Here's a 1-minute tour of how full-stack auth works in Wasp:

    Enabling auth for your app is optional and can be done by configuring the auth field of your app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute"
    }
    }

    //...

    Read more about the auth field options in the API Reference section.

    We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.

    Available auth methods

    Wasp supports the following auth methods:

    Click on each auth method for more details.

    Let's say we enabled the Username & password authentication.

    We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.

    We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.

    We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.

    Protecting a page with authRequired

    When declaring a page, you can set the authRequired property.

    If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.

    main.wasp
    page MainPage {
    component: import Main from "@src/pages/Main",
    authRequired: true
    }
    Requires auth method

    You can only use authRequired if your app uses one of the available auth methods.

    If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.

    Logout action

    We provide an action for logging out the user. Here's how you can use it:

    src/components/LogoutButton.jsx
    import { logout } from 'wasp/client/auth'

    const LogoutButton = () => {
    return <button onClick={logout}>Logout</button>
    }

    Accessing the logged-in user

    You can get access to the user object both on the server and on the client. The user object contains the logged-in user's data.

    The user object has all the fields that you defined in your User entity, plus the auth field which contains the auth identities connected to the user. For example, if the user signed up with their email, the user object might look something like this:

    const user = {
    id: "19c7d164-b5cb-4dde-a0cc-0daea77cf854",

    // Your entity's fields.
    address: "My address",
    // ...

    // Auth identities connected to the user.
    auth: {
    id: "26ab6f96-ed76-4ee5-9ac3-2fd0bf19711f",
    identities: [
    {
    providerName: "email",
    providerUserId: "some@email.com",
    providerData: { ... },
    },
    ]
    },
    }

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    On the client

    There are two ways to access the user object on the client:

    • the user prop
    • the useAuth hook

    Using the user prop

    If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:

    main.wasp
    // ...

    page AccountPage {
    component: import Account from "@src/pages/Account",
    authRequired: true
    }
    src/pages/Account.jsx
    import Button from './Button'
    import { logout } from 'wasp/client/auth'

    const AccountPage = ({ user }) => {
    return (
    <div>
    <Button onClick={logout}>Logout</Button>
    {JSON.stringify(user, null, 2)}
    </div>
    )
    }

    export default AccountPage

    Using the useAuth hook

    Wasp provides a React hook you can use in the client components - useAuth.

    This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.

    src/pages/MainPage.jsx
    import { useAuth, logout } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'
    import Todo from '../Todo'

    export function Main() {
    const { data: user } = useAuth()

    if (!user) {
    return (
    <span>
    Please <Link to="/login">login</Link> or{' '}
    <Link to="/signup">sign up</Link>.
    </span>
    )
    } else {
    return (
    <>
    <button onClick={logout}>Logout</button>
    <Todo />
    </>
    )
    }
    }
    tip

    Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.

    On the server

    Using the context.user object

    When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields and the auth identities connected to the user. We strip out the hashedPassword field from the identities for security reasons.

    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (task, context) => {
    if (!context.user) {
    throw new HttpError(403)
    }

    const Task = context.entities.Task
    return Task.create({
    data: {
    description: task.description,
    user: {
    connect: { id: context.user.id },
    },
    },
    })
    }

    To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.

    When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.

    Sessions

    Wasp's auth uses sessions to keep track of the logged-in user. The session is stored in localStorage on the client and in the database on the server. Under the hood, Wasp uses the excellent Lucia Auth v3 library for session management.

    When users log in, Wasp creates a session for them and stores it in the database. The session is then sent to the client and stored in localStorage. When users log out, Wasp deletes the session from the database and from localStorage.

    User Entity

    Password Hashing

    If you are saving a user's password in the database, you should never save it as plain text. You can use Wasp's helper functions for serializing and deserializing provider data which will automatically hash the password for you:

    main.wasp
    // ...

    action updatePassword {
    fn: import { updatePassword } from "@src/auth",
    }
    src/auth.js
    import {
    createProviderId,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    deserializeAndSanitizeProviderData,
    } from 'wasp/server/auth';

    export const updatePassword = async (args, context) => {
    const providerId = createProviderId('email', args.email)
    const authIdentity = await findAuthIdentity(providerId)
    if (!authIdentity) {
    throw new HttpError(400, "Unknown user")
    }

    const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData)

    // Updates the password and hashes it automatically.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: args.password,
    })
    }

    Default Validations

    When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in Auth UI or if you use the provided auth actions.

    If you decide to create your custom auth actions, you'll need to run the validations yourself.

    Default validations depend on the auth method you use.

    Username & Password

    If you use Username & password authentication, the default validations are:

    • The username must not be empty
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that usernames are stored in a case-insensitive manner.

    Email

    If you use Email authentication, the default validations are:

    • The email must not be empty and a valid email address
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that emails are stored in a case-insensitive manner.

    Customizing the Signup Process

    Sometimes you want to include extra fields in your signup process, like first name and last name and save them in the User entity.

    For this to happen:

    • you need to define the fields that you want saved in the database,
    • you need to customize the SignupForm (in the case of Email or Username & Password auth)

    Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.

    Let's see how to do both.

    1. Defining Extra Fields

    If we want to save some extra fields in our signup process, we need to tell our app they exist.

    We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    * We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

    First, we add the auth.methods.{authMethod}.userSignupFields field in our main.wasp file. The {authMethod} depends on the auth method you are using.

    For example, if you are using Username & Password, you would add the auth.methods.usernameAndPassword.userSignupFields field:

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    address String?
    psl=}

    Then we'll define the userSignupFields object in the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    Read more about the userSignupFields object in the API Reference.

    Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity.

    The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.

    Using Validation Libraries

    You can use any validation library you want to validate the fields. For example, you can use zod like this:

    Click to see the code
    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'
    import * as z from 'zod'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    const AddressSchema = z
    .string({
    required_error: 'Address is required',
    invalid_type_error: 'Address must be a string',
    })
    .min(10, 'Address must be at least 10 characters long')
    const result = AddressSchema.safeParse(data.address)
    if (result.success === false) {
    throw new Error(result.error.issues[0].message)
    }
    return result.data
    },
    })

    Now that we defined the fields, Wasp knows how to:

    1. Validate the data sent from the client
    2. Save the data to the database

    Next, let's see how to customize Auth UI to include those fields.

    2. Customizing the Signup Component

    Using Custom Signup Component

    If you are not using Wasp's Auth UI, you can skip this section. Just make sure to include the extra fields in your custom signup form.

    Read more about using the signup actions for:

    • email auth here
    • username & password auth here

    If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.

    Using a List of Extra Fields

    When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.

    Inside the list, there can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
    2. Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the react-hook-form object and the form state object as arguments.
    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    /* The address field is defined using an object */
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    /* The phone number is defined using a render function */
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    Read more about the extra fields in the API Reference.

    Using a Single Render Function

    Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.

    src/SignupPage.jsx
    import { SignupForm, FormItemGroup } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={(form, state) => {
    const username = form.watch('username')
    return (
    username && (
    <FormItemGroup>
    Hello there <strong>{username}</strong> 👋
    </FormItemGroup>
    )
    )
    }}
    />
    )
    }

    Read more about the render function in the API Reference.

    API Reference

    Auth Fields

    main.wasp
      title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute",
    }
    }

    //...

    app.auth is a dictionary with the following fields:

    userEntity: entity required

    The entity representing the user connected to your business logic.

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    methods: dict required

    A dictionary of auth methods enabled for the app.

    Click on each auth method for more details.

    onAuthFailedRedirectTo: String required

    The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true). Check out these essentials docs on auth to see an example of usage.

    onAuthSucceededRedirectTo: String

    The route to which Wasp will send a successfully authenticated after a successful login/signup. The default value is "/".

    note

    Automatic redirect on successful login only works when using the Wasp-provided Auth UI.

    Signup Fields Customization

    If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.methods.{authMethod}.userSignupFields field in your main.wasp file.

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    Then we'll export the userSignupFields object from the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    The userSignupFields object is an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    If the value that the function received is invalid, the function should throw an error.

    * We exclude the password field from this object to prevent it from being saved as plain text in the database. The password field is handled by Wasp's auth backend.

    SignupForm Customization

    To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.

    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    The extra fields can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.

      The objects have the following properties:

      • name required

        • the name of the field
      • label required

        • the label of the field (used in the UI)
      • type required

        • the type of the field, which can be input or textarea
      • validations

        • an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
    2. Render functions receive the react-hook-form object and the form state as arguments, and they can use them to render arbitrary UI elements.

      The render function has the following signature:

      (form: UseFormReturn, state: FormState) => React.ReactNode
      • form required

        • the react-hook-form object, read more about it in the react-hook-form docs
        • you need to use the form.register function to register your fields
      • state required

        • the form state object which has the following properties:
          • isLoading: boolean
            • whether the form is currently submitting
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/social-auth/github.html b/docs/0.12.0/auth/social-auth/github.html index cb23bdaaaf..3429894ec7 100644 --- a/docs/0.12.0/auth/social-auth/github.html +++ b/docs/0.12.0/auth/social-auth/github.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.

    Letting your users log in using their GitHub accounts turns the signup process into a breeze.

    Let's walk through enabling Github Authentication, explain some of the default settings, and show how to override them.

    Setting up Github Auth

    Enabling GitHub Authentication comes down to a series of steps:

    1. Enabling GitHub authentication in the Wasp file.
    2. Adding the User entity.
    3. Creating a GitHub OAuth app.
    4. Adding the neccessary Routes and Pages
    5. Using Auth UI components in our Pages.

    Here's a skeleton of how our main.wasp should look like after we're done:

    main.wasp
    // Configuring the social authentication
    app myApp {
    auth: { ... }
    }

    // Defining entities
    entity User { ... }

    // Defining routes and pages
    route LoginRoute { ... }
    page LoginPage { ... }

    1. Adding Github Auth to Your Wasp File

    Let's start by properly configuring the Auth object:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the User entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable Github Auth
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    2. Add the User Entity

    Let's now define the app.auth.userEntity entity:

    main.wasp
    // ...
    // 3. Define the User entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // ...
    psl=}

    3. Creating a GitHub OAuth App

    To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it:

    1. Log into your GitHub account and navigate to: https://github.com/settings/developers.
    2. Select New OAuth App.
    3. Supply required information.
    GitHub Applications Screenshot
    • For Authorization callback URL:
      • For development, put: http://localhost:3000/auth/login/github.
      • Once you know on which URL your app will be deployed, you can create a new app with that URL instead e.g. https://someotherhost.com/auth/login/github.
    1. Hit Register application.
    2. Hit Generate a new client secret on the next page.
    3. Copy your Client ID and Client secret as you'll need them in the next step.

    4. Adding Environment Variables

    Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

    .env.server
    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret

    5. Adding the Necessary Routes and Pages

    Let's define the necessary authentication Routes and Pages.

    Add the following code to your main.wasp file:

    main.wasp
    // ...

    // 6. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    6. Creating the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }

    We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    Yay, we've successfully set up Github Auth! 🎉

    Github Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add gitHub: {} to the auth.methods dictionary to use it with default settings.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Using the User's Provider Account Details

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    psl=}

    // ...
    src/auth/github.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.displayName,
    };

    export function getConfig() {
    return {
    clientID // look up from env or elsewhere
    clientSecret // look up from env or elsewhere
    scope: [],
    };
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The gitHub dict has the following properties:

    • configFn: ExtImport

      This function should return an object with the Client ID, Client Secret, and scope for the OAuth provider.

      src/auth/github.js
      export function getConfig() {
      return {
      clientID, // look up from env or elsewhere
      clientSecret, // look up from env or elsewhere
      scope: [],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })
      Read more about the `userSignupFields` function [here](../overview#1-defining-extra-fields).
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/social-auth/google.html b/docs/0.12.0/auth/social-auth/google.html index a5faf9146f..4aada86a14 100644 --- a/docs/0.12.0/auth/social-auth/google.html +++ b/docs/0.12.0/auth/social-auth/google.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It should have the following code:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }
    Auth UI

    Our pages use an automatically-generated Auth UI component. Read more about Auth UI components here.

    Conclusion

    Yay, we've successfully set up Google Auth! 🎉

    Google Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add google: {} to the auth.methods dictionary to use it with default settings:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Using the User's Provider Account Details

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    psl=}

    // ...
    src/auth/google.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.displayName,
    }

    export function getConfig() {
    return {
    clientID, // look up from env or elsewhere
    clientSecret, // look up from env or elsewhere
    scope: ['profile', 'email'],
    }
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The google dict has the following properties:

    • configFn: ExtImport

      This function must return an object with the Client ID, the Client Secret, and the scope for the OAuth provider.

      src/auth/google.js
      export function getConfig() {
      return {
      clientID, // look up from env or elsewhere
      clientSecret, // look up from env or elsewhere
      scope: ['profile', 'email'],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })
      Read more about the `userSignupFields` function [here](../overview#1-defining-extra-fields).
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/social-auth/overview.html b/docs/0.12.0/auth/social-auth/overview.html index 011ccb8b2c..5bedc17f7c 100644 --- a/docs/0.12.0/auth/social-auth/overview.html +++ b/docs/0.12.0/auth/social-auth/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -31,7 +31,7 @@ Depending on the flag's value, you can redirect users to the appropriate signup step.

    For example:

    1. When the user lands on the homepage, check the value of user.isSignupComplete.
    2. If it's false, it means the user has started the signup process but hasn't yet chosen their username. Therefore, you can redirect them to EditUserDetailsPage where they can edit the username property.
    src/HomePage.jsx
    import { useAuth } from 'wasp/client/auth'
    import { Redirect } from 'react-router-dom'

    export function HomePage() {
    const { data: user } = useAuth()

    if (user.isSignupComplete === false) {
    return <Redirect to="/edit-user-details" />
    }

    // ...
    }

    Using the User's Provider Account Details

    Account details are provider-specific. Each provider has their own rules for defining the userSignupFields and configFn fields:

    UI Helpers

    Use Auth UI

    Auth UI is a common name for all high-level auth forms that come with Wasp.

    These include fully functional auto-generated login and signup forms with working social login buttons. If you're looking for the fastest way to get your auth up and running, that's where you should look.

    The UI helpers described below are lower-level and are useful for creating your custom forms.

    Wasp provides sign-in buttons and URLs for each of the supported social login providers.

    src/LoginPage.jsx
    import {
    GoogleSignInButton,
    googleSignInUrl,
    GitHubSignInButton,
    gitHubSignInUrl,
    } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <>
    <GoogleSignInButton />
    <GitHubSignInButton />
    {/* or */}
    <a href={googleSignInUrl}>Sign in with Google</a>
    <a href={gitHubSignInUrl}>Sign in with GitHub</a>
    </>
    )
    }

    If you need even more customization, you can create your custom components using signInUrls.

    API Reference

    Fields in the app.auth Dictionary and Overrides

    For more information on:

    • Allowed fields in app.auth
    • userSignupFields and configFn functions

    Check the provider-specific API References:

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/ui.html b/docs/0.12.0/auth/ui.html index a7edcf7622..2cb9fea22d 100644 --- a/docs/0.12.0/auth/ui.html +++ b/docs/0.12.0/auth/ui.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Auth UI

    To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

    Below we cover all of the available UI components and how to use them.

    Auth UI

    Overview

    After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

    Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    },
    // ...
    }
    }

    You'll get the following UI:

    Auth UI

    And then if you enable Google and Github:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    google: {},
    github: {},
    },
    // ...
    }
    }

    The form will automatically update to look like this:

    Auth UI

    Let's go through all of the available components and how to use them.

    Auth Components

    The following components are available for you to use in your app:

    Login Form

    Used with Username & Password, Email, Github and Google authentication.

    Login form

    You can use the LoginForm component to build your login page:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage.jsx"
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    // Use it like this
    export function LoginPage() {
    return <LoginForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Signup Form

    Used with Username & Password, Email, Github and Google authentication.

    Signup form

    You can use the SignupForm component to build your signup page:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage.jsx"
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    // Use it like this
    export function SignupPage() {
    return <SignupForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Read more about customizing the signup process like adding additional fields or extra UI in the Auth Overview section.

    Forgot Password Form

    Used with Email authentication.

    If users forget their password, they can use this form to reset it.

    Forgot password form

    You can use the ForgotPasswordForm component to build your own forgot password page:

    main.wasp
    // ...

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage.jsx"
    }
    src/ForgotPasswordPage.jsx
    import { ForgotPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ForgotPasswordPage() {
    return <ForgotPasswordForm />
    }

    Reset Password Form

    Used with Email authentication.

    After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

    Reset password form

    You can use the ResetPasswordForm component to build your reset password page:

    main.wasp
    // ...

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { ResetPasswordPage } from "@src/ResetPasswordPage.jsx"
    }
    src/ResetPasswordPage.jsx
    import { ResetPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ResetPasswordPage() {
    return <ResetPasswordForm />
    }

    Verify Email Form

    Used with Email authentication.

    After users sign up, they will receive an email with a link to this form where they can verify their email.

    Verify email form

    You can use the VerifyEmailForm component to build your email verification page:

    main.wasp
    // ...

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { VerifyEmailPage } from "@src/VerifyEmailPage.jsx"
    }
    src/VerifyEmailPage.jsx
    import { VerifyEmailForm } from 'wasp/client/auth'

    // Use it like this
    export function VerifyEmailPage() {
    return <VerifyEmailForm />
    }

    Customization 💅🏻

    You customize all of the available forms by passing props to them.

    Props you can pass to all of the forms:

    1. appearance - customize the form colors (via design tokens)
    2. logo - path to your logo
    3. socialLayout - layout of the social buttons, which can be vertical or horizontal

    1. Customizing the Colors

    We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

    List of all available tokens

    See the list of all available tokens which you can override.

    src/appearance.js
    export const authAppearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { authAppearance } from './appearance'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass the appearance object to the form
    appearance={authAppearance}
    />
    )
    }

    We recommend defining your appearance in a separate file and importing it into your components.

    You can add your logo to the Auth UI by passing the logo prop to any of the components.

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import Logo from './logo.png'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the path to your logo
    logo={Logo}
    />
    )
    }

    3. Social Buttons Layout

    You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

    If we pass in vertical:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the socialLayout prop
    socialLayout="vertical"
    />
    )
    }

    We get this:

    Vertical social buttons

    Let's Put Everything Together 🪄

    If we provide the logo and custom colors:

    src/appearance.js
    export const appearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    import { authAppearance } from './appearance'
    import todoLogo from './todoLogo.png'

    export function LoginPage() {
    return <LoginForm appearance={appearance} logo={todoLogo} />
    }

    We get a form looking like this:

    Custom login form
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/auth/username-and-pass.html b/docs/0.12.0/auth/username-and-pass.html index 8a60ef1fd5..3075e0f816 100644 --- a/docs/0.12.0/auth/username-and-pass.html +++ b/docs/0.12.0/auth/username-and-pass.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Username & Password

    Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.

    Setting Up Username & Password Authentication

    To set up username authentication we need to:

    1. Enable username authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }
    // Defining User entity
    entity User { ... }
    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Username Authentication

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable username authentication
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }

    Read more about the usernameAndPassword auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    main.wasp
    // 3. Define the user entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    psl=}

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...
    // 4. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }
    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm, SignupForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    That's it! We have set up username authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the auth overview docs.

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Customizing the Auth Flow

    The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

    Read more about the default username and password validation rules in the auth overview docs.

    If you require more control in your authentication flow, you can achieve that in the following ways:

    1. Create your UI and use signup and login actions.
    2. Create your custom sign-up action which uses the lower-level API, along with your custom code.

    1. Using the signup and login actions

    login()

    An action for logging in the user.

    It takes two arguments:

    • username: string required

      Username of the user logging in.

    • password: string required

      Password of the user logging in.

    You can use it like this:

    src/pages/auth.jsx
    import { login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory, Link } from 'react-router-dom'

    export function LoginPage() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await login(username, password)
    history.push('/')
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }
    note

    When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

    signup()

    An action for signing up the user. This action does not log in the user, you still need to call login().

    It takes one argument:

    • userFields: object required

      It has the following fields:

      • username: string required

      • password: string required

      info

      By default, Wasp will only save the username and password fields. If you want to add extra fields to your signup process, read about defining extra signup fields.

    You can use it like this:

    src/pages/auth.jsx
    import { signup, login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory } from 'react-router-dom'
    import { Link } from 'react-router-dom'

    export function Signup() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await signup({
    username,
    password,
    })
    await login(username, password)
    history.push("/")
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }

    2. Creating your custom sign-up action

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidUsername,
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidUsername(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('username', args.username)
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Username

    • ensureValidUsername(args)

      Checks if the username is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    getUsername

    If you are looking to access the user's username in your code, you can do that by accessing the info about the user that is stored in the user.auth.identities array.

    To make things a bit easier for you, Wasp offers the getUsername helper.

    The getUsername helper returns the user's username or null if the user doesn't have a username auth identity.

    src/MainPage.jsx
    import { getUsername } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const username = getUsername(user)
    // ...
    }
    src/tasks.js
    import { getUsername } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const username = getUsername(context.user)
    // ...
    }

    API Reference

    userEntity fields

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the usernameAndPassword dict

    main.wasp
    app myApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/email.js",
    },
    },
    onAuthFailedRedirectTo: "/login"
    }
    }
    // ...

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })
    Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields).
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/contact.html b/docs/0.12.0/contact.html index d0b9aa4ffb..0e5cb4d5b1 100644 --- a/docs/0.12.0/contact.html +++ b/docs/0.12.0/contact.html @@ -19,13 +19,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/0.12.0/contributing.html b/docs/0.12.0/contributing.html index cb3f053def..8633324700 100644 --- a/docs/0.12.0/contributing.html +++ b/docs/0.12.0/contributing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Contributing

    Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

    Some side notes to make your journey easier:

    1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

    2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

    3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

    Happy hacking!

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/backends.html b/docs/0.12.0/data-model/backends.html index 974ebe8a58..bfa649244f 100644 --- a/docs/0.12.0/data-model/backends.html +++ b/docs/0.12.0/data-model/backends.html @@ -19,8 +19,8 @@ - - + +
    @@ -36,7 +36,7 @@ Whenever you modify the db.system field, make sure to run wasp db migrate-dev to apply the changes.

  • seeds: [ExtImport]

    Defines the seed functions you can use with the wasp db seed command to seed your database with initial data. Read the Seeding section for more details.

  • prisma: PrismaOptions

    Additional configuration for Prisma.

    main.wasp
    app MyApp {
    // ...
    db: {
    // ...
    prisma: {
    clientPreviewFeatures: ["postgresqlExtensions"],
    dbExtensions: [
    { name: "hstore", schema: "myHstoreSchema" },
    { name: "pg_trgm" },
    { name: "postgis", version: "2.1" },
    ]
    }
    }
    }

    It's a dictionary with the following fields:

    • clientPreviewFeatures : [string]

      Allows you to define Prisma client preview features, like for example, "postgresqlExtensions".

    • dbExtensions: DbExtension[]

      It allows you to define PostgreSQL extensions that should be enabled for your database. Read more about PostgreSQL extensions in Prisma.

      For each extension you define a dict with the following fields:

      • name: string required

        The name of the extension you would normally put in the Prisma file.

        schema.prisma
        extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
        // 👆 Extension name
      • map: string

        It sets the map argument of the extension. Explanation for the field from the Prisma docs:

        This is the database name of the extension. If this argument is not specified, the name of the extension in the Prisma schema must match the database name.

      • schema: string

        It sets the schema argument of the extension. Explanation for the field from the Prisma docs:

        This is the name of the schema in which to activate the extension's objects. If this argument is not specified, the current default object creation schema is used.

      • version: string

        It sets the version argument of the extension. Explanation for the field from the Prisma docs:

        This is the version of the extension to activate. If this argument is not specified, the value given in the extension's control file is used.

  • CLI Commands for Seeding the Database

    Use one of the following commands to run the seed functions:

    • wasp db seed

      If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.

    • wasp db seed <seed-name>

      This command runs the seed function with the specified name. The name is the identifier used in its import expression in the app.db.seeds list. For example, to run the seed function devSeedSimple which was defined like this:

      main.wasp
      app MyApp {
      // ...
      db: {
      // ...
      seeds: [
      // ...
      import { devSeedSimple } from "@src/dbSeeds.js",
      ]
      }
      }

      Use the following command:

      wasp db seed devSeedSimple
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/crud.html b/docs/0.12.0/data-model/crud.html index 4cb9a72170..23a84a1de0 100644 --- a/docs/0.12.0/data-model/crud.html +++ b/docs/0.12.0/data-model/crud.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Automatic CRUD

    If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.

    Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.

    With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting Entities. As you update definitions for your Entities, Wasp automatically regenerates the backend logic.

    Early preview

    This feature is currently in early preview and we are actively working on it. Read more about our plans for CRUD operations.

    Overview

    Imagine we have a Task entity and we want to enable CRUD operations for it.

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    psl=}

    We can then define a new crud called Tasks.

    We specify to use the Task entity and we enable the getAll, get, create and update operations (let's say we don't need the delete operation).

    main.wasp
    crud Tasks {
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // by default only logged in users can perform operations
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js",
    },
    update: {},
    },
    }
    1. It uses default implementation for getAll, get, and update,
    2. ... while specifying a custom implementation for create.
    3. getAll will be public (no auth needed), while the rest of the operations will be private.

    Here's what it looks like when visualized:

    Automatic CRUD with Wasp
    Visualization of the Tasks crud declaration

    We can now use the CRUD queries and actions we just specified in our client code.

    Keep reading for an example of Automatic CRUD in action, or skip ahead for the API Reference

    Example: A Simple TODO App

    Let's create a full-app example that uses automatic CRUD. We'll stick to using the Task entity from the previous example, but we'll add a User entity and enable username and password based auth.

    Automatic CRUD with Wasp
    We are building a simple tasks app with username based auth

    Creating the App

    We can start by running wasp new tasksCrudApp and then adding the following to the main.wasp file:

    main.wasp
    app tasksCrudApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Tasks Crud App",

    // We enabled auth and set the auth method to username and password
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    tasks Task[]
    psl=}

    // We defined a Task entity on which we'll enable CRUD later on
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    psl=}

    // Tasks app routes
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@src/MainPage.jsx",
    authRequired: true,
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage.jsx",
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage.jsx",
    }

    We can then run wasp db migrate-dev to create the database and run the migrations.

    Adding CRUD to the Task Entity ✨

    Let's add the following crud declaration to our main.wasp file:

    main.wasp
    // ...

    crud Tasks {
    entity: Task,
    operations: {
    getAll: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js",
    },
    },
    }

    You'll notice that we enabled only getAll and create operations. This means that only these operations will be available.

    We also overrode the create operation with a custom implementation. This means that the create operation will not be generated, but instead, the createTask function from @src/tasks.js will be used.

    Our Custom create Operation

    Here's the src/tasks.ts file:

    src/tasks.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    We made a custom create operation because we want to make sure that the task is connected to the user that is creating it. Automatic CRUD doesn't support this by default (yet!). Read more about the default implementations here.

    Using the Generated CRUD Operations on the Client

    And let's use the generated operations in our client code:

    src/MainPage.jsx
    import { Tasks } from 'wasp/client/crud'
    import { useState } from 'react'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
    const createTask = Tasks.create.useAction()
    const [taskDescription, setTaskDescription] = useState('')

    function handleCreateTask() {
    createTask({ description: taskDescription, isDone: false })
    setTaskDescription('')
    }

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    return (
    <div
    style={{
    fontSize: '1.5rem',
    display: 'grid',
    placeContent: 'center',
    height: '100vh',
    }}
    >
    <div>
    <input
    value={taskDescription}
    onChange={(e) => setTaskDescription(e.target.value)}
    />
    <button onClick={handleCreateTask}>Create task</button>
    </div>
    <ul>
    {tasks.map((task) => (
    <li key={task.id}>{task.description}</li>
    ))}
    </ul>
    </div>
    )
    }

    And here are the login and signup pages, where we are using Wasp's Auth UI components:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function LoginPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <LoginForm />
    <div>
    <Link to="/signup">Create an account</Link>
    </div>
    </div>
    )
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    export function SignupPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <SignupForm />
    </div>
    )
    }

    That's it. You can now run wasp start and see the app in action. ⚡️

    You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.

    Future of CRUD Operations in Wasp

    CRUD operations currently have a limited set of knowledge about the business logic they are implementing.

    • For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the create operation in the example above.
    • Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
    • Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.

    CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.

    We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on this GitHub issue to see how we are doing.

    API Reference

    CRUD declaration work on top of existing entity declaration. We'll fully explore the API using two examples:

    1. A basic CRUD declaration that relies on default options.
    2. A more involved CRUD declaration that uses extra options and overrides.

    Declaring a CRUD With Default Options

    If we create CRUD operations for an entity named Task, like this:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    get: {},
    getAll: {},
    create: {},
    update: {},
    delete: {},
    },
    }

    Wasp will give you the following default implementations:

    get - returns one entity based on the id field

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.findUnique({ where: { id: args.id } })

    getAll - returns all entities

    // ...

    // If the operation is not public, Wasp checks if an authenticated user
    // is making the request.

    return Task.findMany()

    create - creates a new entity

    // ...
    return Task.create({ data: args.data })

    update - updates an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.update({ where: { id: args.id }, data: args.data })

    delete - deletes an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.delete({ where: { id: args.id } })
    Current Limitations

    In the default create and update implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.

    In the future, we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.

    For now, the solution is to provide an override function. You can override the default implementation by using the overrideFn option and implementing the validation logic yourself.

    Declaring a CRUD With All Available Options

    Here's an example of a more complex CRUD declaration:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // optional, defaults to false
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js", // optional
    },
    update: {},
    },
    }

    The CRUD declaration features the following fields:

    • entity: Entity required

      The entity to which the CRUD operations will be applied.

    • operations: { [operationName]: CrudOperationOptions } required

      The operations to be generated. The key is the name of the operation, and the value is the operation configuration.

      • The possible values for operationName are:
        • getAll
        • get
        • create
        • update
        • delete
      • CrudOperationOptions can have the following fields:
        • isPublic: bool - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to false.
        • overrideFn: ExtImport - The import statement of the optional override implementation in Node.js.

    Defining the overrides

    Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:

    • args

      The arguments of the operation i.e. the data sent from the client.

    • context

      Context contains the user making the request and the entities object with the entity that's being operated on.

    For a usage example, check the example guide.

    Using the CRUD operations in client code

    On the client, you import the CRUD operations from wasp/client/crud by import the {crud name} object. For example, if you have a CRUD called Tasks, you would import the operations like this:

    SomePage.jsx
    import { Tasks } from 'wasp/client/crud'

    You can then access the operations like this:

    SomePage.jsx
    const { data } = Tasks.getAll.useQuery()
    const { data } = Tasks.get.useQuery({ id: 1 })
    const createAction = Tasks.create.useAction()
    const updateAction = Tasks.update.useAction()
    const deleteAction = Tasks.delete.useAction()

    All CRUD operations are implemented with Queries and Actions under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)


    Join our community on Discord, where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/entities.html b/docs/0.12.0/data-model/entities.html index bd54e2d7ce..b52c65176b 100644 --- a/docs/0.12.0/data-model/entities.html +++ b/docs/0.12.0/data-model/entities.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. Wasp Entities directly correspond to Prisma's data model. Still, you don't need to be familiar with Prisma to effectively use Wasp, as it comes with a simple API wrapper for working with Prisma's core features.

    The only requirement for defining Wasp Entities is familiarity with the Prisma Schema Language (PSL), a simple definition language explicitly created for defining models in Prisma. The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:

    Defining an Entity

    As mentioned, an entity declaration represents a database model.

    Each Entity declaration corresponds 1-to-1 to Prisma's data model. Here's how you could define an Entity that represents a Task:

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}

    Let's go through this declaration in detail:

    • entity Task - This tells Wasp that we wish to define an Entity (i.e., database model) called Task. Wasp automatically creates a table called tasks.
    • {=psl ... psl=} - Wasp treats everything that comes between the two psl tags as PSL (Prisma Schema Language).

    The above PSL definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the tasks table has three columns):

    • id - An integer value serving as a primary key. The database automatically generates it by incrementing the previously generated id.
    • description - A string value for storing the task's description.
    • isDone - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to false by default.

    Working with Entities

    Let's see how you can define and work with Wasp Entities:

    1. Create/update some Entities in your .wasp file.
    2. Run wasp db migrate-dev. This command syncs the database model with the Entity definitions in your .wasp file. It does this by creating migration scripts.
    3. Migration scripts are automatically placed in the migrations/ folder. Make sure to commit this folder into version control.
    4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about operations).

    Using Entities in Operations

    Most of the time, you will be working with Entities within the context of Operations (Queries & Actions). We'll see how that's done on the next page.

    Using Entities directly

    If you need more control, you can directly interact with Entities by importing and using the Prisma Client. We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.

    You can only use the Prisma Client in your Wasp server code. You can import it like this:

    import { prisma } from 'wasp/server'

    prisma.task.create({
    description: "Read the Entities doc",
    isDone: true // almost :)
    })

    Next steps

    Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/operations/actions.html b/docs/0.12.0/data-model/operations/actions.html index eb6d9673e0..a431849f87 100644 --- a/docs/0.12.0/data-model/operations/actions.html +++ b/docs/0.12.0/data-model/operations/actions.html @@ -19,8 +19,8 @@ - - + +
    @@ -42,7 +42,7 @@ Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

    1. args (type depends on the Action)

      An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

    2. context (type depends on the Action)

      An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

    Example

    The following Action:

    action createFoo {
    fn: import { createFoo } from "@src/actions.js"
    entities: [Foo]
    }

    Expects to find a named export createfoo from the file src/actions.js

    actions.js
    export const createFoo = (args, context) => {
    // implementation
    }

    The useAction Hook and Optimistic Updates

    Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

    When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

    The useAction hook accepts two arguments:

    • actionFn required

      The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

    • actionOptions

      An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

      • optimisticUpdates

        An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

        • getQuerySpecifier required

        A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).

        • updateQuery required

        The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

        • item - The argument you pass into the decorated Action.
        • oldData - The currently cached value for the Query identified by the specifier.
    caution

    The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

    Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

    Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

    Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

    src/pages/Task.jsx
    import React from 'react'
    import {
    useQuery,
    useAction,
    getTask,
    markTaskAsDone,
    } from 'wasp/client/operations'

    const TaskPage = ({ id }) => {
    const { data: task } = useQuery(getTask, { id })
    const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
    optimisticUpdates: [
    {
    getQuerySpecifier: ({ id }) => [getTask, { id }],
    updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
    },
    ],
    })

    if (!task) {
    return <h1>"Loading"</h1>
    }

    const { description, isDone } = task
    return (
    <div>
    <p>
    <strong>Description: </strong>
    {description}
    </p>
    <p>
    <strong>Is done: </strong>
    {isDone ? 'Yes' : 'No'}
    </p>
    {isDone || (
    <button onClick={() => markTaskAsDoneOptimistically({ id })}>
    Mark as done.
    </button>
    )}
    </div>
    )
    }

    export default TaskPage

    Advanced usage

    The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

    Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

    If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

    import { getTasks } from 'wasp/client/operations'

    const queryKey = getTasks.queryCacheKey
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/operations/overview.html b/docs/0.12.0/data-model/operations/overview.html index dbe730e0ae..dcc13ca71f 100644 --- a/docs/0.12.0/data-model/operations/overview.html +++ b/docs/0.12.0/data-model/operations/overview.html @@ -19,14 +19,14 @@ - - + +
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/data-model/operations/queries.html b/docs/0.12.0/data-model/operations/queries.html index 53324c75ae..45a040fc43 100644 --- a/docs/0.12.0/data-model/operations/queries.html +++ b/docs/0.12.0/data-model/operations/queries.html @@ -19,8 +19,8 @@ - - + +
    @@ -46,7 +46,7 @@ behavior for this particular Query. If you want to change the global defaults, you can do so in the client setup function.

    For an example of usage, check this section.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/editor-setup.html b/docs/0.12.0/editor-setup.html index 1b68344eb6..c0f8e953b4 100644 --- a/docs/0.12.0/editor-setup.html +++ b/docs/0.12.0/editor-setup.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/general/cli.html b/docs/0.12.0/general/cli.html index bea65aaa35..3a4efb3ad1 100644 --- a/docs/0.12.0/general/cli.html +++ b/docs/0.12.0/general/cli.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    new:ai <app-name> <app-description> [<config-json>]
    Uses AI to create a new Wasp project just based on the app name and the description.
    You can do the same thing with `wasp new` interactively.
    Run `wasp new:ai` for more info.

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code, all cached artifacts, and the node_modules dir.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about the current Wasp project.
    test Executes tests in your project.
    studio (experimental) GUI for inspecting your Wasp app.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      Simple starter template with a single page.
      [2] todo-ts
      Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
      [3] saas
      Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
      [4] embeddings
      Comes with code for generating vector embeddings and performing vector similarity search.
      [5] ai-generated
      🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
      ▸ 1

      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      🐝 --- Deleting the .wasp/ directory... -------------------------------------------

      ✅ --- Deleted the .wasp/ directory. ----------------------------------------------

      🐝 --- Deleting the node_modules/ directory... ------------------------------------

      ✅ --- Deleted the node_modules/ directory. ---------------------------------------
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    • wasp studio shows you an graphical overview of your application in a graph: pages, queries, actions, data model etc.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.12.0

      If you wish to install/switch to the latest version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s

      If you want specific x.y.z version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v x.y.z

      Check https://github.com/wasp-lang/wasp/releases for the list of valid versions, including the latest one.
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/general/language.html b/docs/0.12.0/general/language.html index c58521ab79..2e84a3f68e 100644 --- a/docs/0.12.0/general/language.html +++ b/docs/0.12.0/general/language.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Wasp Language (.wasp)

    Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

    It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

    It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

    Declarations

    The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

    app MyApp {
    title: "My app"
    }

    route RootRoute { path: "/", to: DashboardPage }

    page DashboardPage {
    component: import { DashboardPage } from "@src/Dashboard.jsx"
    }

    In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

    Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

    • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
    • <declaration_name> is an identifier chosen by you to name this specific declaration
    • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

    So, for app declaration above, we have:

    • declaration type app
    • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
    • declaration body { title: "My app" }, which is a dictionary with field title that has string value. Type of this dictionary is in line with the declaration body type of the app declaration type. If we provided something else, e.g. changed title to little, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for app.

    Each declaration has a meaning behind it that describes how your web app should behave and function.

    All the other types in Wasp language (primitive types (string, number), composite types (dict, list), enum types (DbSystem), ...) are used to define the declaration bodies.

    Complete List of Wasp Types

    Wasp's type system can be divided into two main categories of types: fundamental types and domain types.

    While fundamental types are here to be basic building blocks of a a language, and are very similar to what you would see in other popular languages, domain types are what makes Wasp special, as they model the concepts of a web app like page, route and similar.

    • Fundamental types (source of truth)
      • Primitive types
        • string ("foo", "they said: \"hi\"")
        • bool (true, false)
        • number (12, 14.5)
        • declaration reference (name of existing declaration: TaskPage, updateTask)
        • ExtImport (external import) (import Foo from "@src/bar.js", import { Smth } from "@src/a/b.js")
          • The path has to start with "@src". The rest is relative to the src directory.
          • Import has to be a default import import Foo or a single named import import { Foo }.
        • json ({=json { a: 5, b: ["hi"] } json=})
        • psl (Prisma Schema Language) ({=psl <psl data model syntax> psl=})
      • Composite types
        • dict (dictionary) ({ a: 5, b: "foo" })
        • list ([1, 2, 3])
        • tuple ((1, "bar"), (2, 4, true))
          • Tuples can be of size 2, 3 and 4.
    • Domain types (source of truth)
      • Declaration types
        • action
        • api
        • apiNamespace
        • app
        • entity
        • job
        • page
        • query
        • route
        • crud
      • Enum types
        • DbSystem
        • HttpMethod
        • JobExecutor
        • EmailProvider

    You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/migrate-from-0-11-to-0-12.html b/docs/0.12.0/migrate-from-0-11-to-0-12.html index 8361f5d6b9..745c5059a0 100644 --- a/docs/0.12.0/migrate-from-0-11-to-0-12.html +++ b/docs/0.12.0/migrate-from-0-11-to-0-12.html @@ -19,8 +19,8 @@ - - + +
    @@ -56,7 +56,7 @@ src/server), you are now free to reorganize your project however you think is best, as long as you keep all the source files in the src/ directory.

    This section is optional, but if you didn't like the server/client separation, now's the perfect time to change it.

    For example, if your src dir looked like this:

    src

    ├── client
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── MainPage.tsx
    │   ├── Register.tsx
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   ├── Task.tsx
    │   └── User.tsx
    ├── server
    │   ├── taskActions.ts
    │   ├── taskQueries.ts
    │   ├── userActions.ts
    │   └── userQueries.ts
    └── shared
    └── utils.ts

    you can now change it to a feature-based structure (which we recommend for any project that is not very small):

    src

    ├── task
    │   ├── actions.ts -- former taskActions.ts
    │   ├── queries.ts -- former taskQueries.ts
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   └── Task.tsx
    ├── user
    │   ├── actions.ts -- former userActions.ts
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── queries.ts -- former userQueries.ts
    │   ├── Register.tsx
    │   └── User.tsx
    ├── MainPage.tsx
    └── utils.ts

    Appendix

    Example Data Migration Functions

    The migration functions provided below are written with the typical use cases in mind and you can use them as-is. If your setup requires additional logic, you can use them as a good starting point and modify them to your needs.

    Note that all of the functions below are written to be idempotent, meaning that running a function multiple times can't hurt. This allows executing a function again in case only a part of the previous execution succeeded and also means that accidentally running it one time too much won't have any negative effects. We recommend you keep your data migration functions idempotent.

    Username & Password

    To successfully migrate the users using the Username & Password auth method, you will need to do two things:

    1. Migrate the user data

      Username & Password data migration function
      main.wasp
      api migrateUsernameAndPassword {
      httpRoute: (GET, "/migrate-username-and-password"),
      fn: import { migrateUsernameAndPasswordHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type UsernameProviderData } from "wasp/server/auth";
      import { MigrateUsernameAndPassword } from "wasp/server/api";

      export const migrateUsernameAndPasswordHandler: MigrateUsernameAndPassword =
      async (_req, res) => {
      const result = await migrateUsernameAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateUsernameAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.username || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using username auth) with id:", user.id);
      continue;
      }

      const providerData: UsernameProviderData = {
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "username";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.username.toLowerCase(),
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Provide a way for users to migrate their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to migrate their password after the migration, as the old password will no longer work.

      Since the only way users using username and password as a login method can verify their identity is by providing both their username and password (there is no email or any other info, unless you asked for it and stored it explicitly), we need to provide them a way to exchange their old password for a new password. One way to handle this is to inform them about the need to migrate their password (on the login page) and provide a custom page to migrate the password.

    Steps to create a custom page for migrating the password
    1. You will need to install the secure-password and sodium-native packages to use the old hashing algorithm:

      npm install secure-password@4.0.0 sodium-native@3.3.0 --save-exact

      Make sure to save the exact versions of the packages.

    2. Then you'll need to create a new page in your app where users can migrate their password. You can use the following code as a starting point:

    main.wasp
    route MigratePasswordRoute { path: "/migrate-password", to: MigratePassword }
    page MigratePassword {
    component: import { MigratePasswordPage } from "@src/pages/MigratePassword"
    }
    src/pages/MigratePassword.jsx
    import {
    FormItemGroup,
    FormLabel,
    FormInput,
    FormError,
    } from "wasp/client/auth";
    import { useForm } from "react-hook-form";
    import { migratePassword } from "wasp/client/operations";
    import { useState } from "react";

    export function MigratePasswordPage() {
    const [successMessage, setSuccessMessage] = useState(null);
    const [errorMessage, setErrorMessage] = useState(null);
    const form = useForm();

    const onSubmit = form.handleSubmit(async (data) => {
    try {
    const result = await migratePassword(data);
    setSuccessMessage(result.message);
    } catch (e) {
    console.error(e);
    if (e instanceof Error) {
    setErrorMessage(e.message);
    }
    }
    });

    return (
    <div style={{
    maxWidth: "400px",
    margin: "auto",
    }}>
    <h1>Migrate your password</h1>
    <p>
    If you have an account on the old version of the website, you can
    migrate your password to the new version.
    </p>
    {successMessage && <div>{successMessage}</div>}
    {errorMessage && <FormError>{errorMessage}</FormError>}
    <form onSubmit={onSubmit}>
    <FormItemGroup>
    <FormLabel>Username</FormLabel>
    <FormInput
    {...form.register("username", {
    required: "Username is required",
    })}
    />
    <FormError>{form.formState.errors.username?.message}</FormError>
    </FormItemGroup>
    <FormItemGroup>
    <FormLabel>Password</FormLabel>
    <FormInput
    {...form.register("password", {
    required: "Password is required",
    })}
    type="password"
    />
    <FormError>{form.formState.errors.password?.message}</FormError>
    </FormItemGroup>
    <button type="submit">Migrate password</button>
    </form>
    </div>
    );
    }
    1. Finally, you will need to create a new operation in your app to handle the password migration. You can use the following code as a starting point:
    main.wasp
    action migratePassword {
    fn: import { migratePassword } from "@src/auth",
    entities: []
    }
    src/auth.js
    import SecurePassword from "secure-password";
    import { HttpError } from "wasp/server";
    import {
    createProviderId,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    } from "wasp/server/auth";

    export const migratePassword = async ({ password, username }, _context) => {
    const providerId = createProviderId("username", username);
    const authIdentity = await findAuthIdentity(providerId);

    if (!authIdentity) {
    throw new HttpError(400, "Something went wrong");
    }

    const providerData = deserializeAndSanitizeProviderData(
    authIdentity.providerData
    );

    try {
    const SP = new SecurePassword();

    // This will verify the password using the old algorithm
    const result = await SP.verify(
    Buffer.from(password),
    Buffer.from(providerData.hashedPassword, "base64")
    );

    if (result !== SecurePassword.VALID) {
    throw new HttpError(400, "Something went wrong");
    }

    // This will hash the password using the new algorithm and update the
    // provider data in the database.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: password,
    });
    } catch (e) {
    throw new HttpError(400, "Something went wrong");
    }

    return {
    message: "Password migrated successfully.",
    };
    };

    Email

    To successfully migrate the users using the Email auth method, you will need to do two things:

    1. Migrate the user data

      Email data migration function
      main.wasp
      api migrateEmail {
      httpRoute: (GET, "/migrate-email"),
      fn: import { migrateEmailHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type EmailProviderData } from "wasp/server/auth";
      import { MigrateEmail } from "wasp/server/api";

      export const migrateEmailHandler: MigrateEmail =
      async (_req, res) => {
      const result = await migrateEmailAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateEmailAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.email || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using email auth) with id:", user.id);
      continue;
      }

      const providerData: EmailProviderData = {
      isEmailVerified: user.isEmailVerified,
      emailVerificationSentAt:
      user.emailVerificationSentAt?.toISOString() ?? null,
      passwordResetSentAt: user.passwordResetSentAt?.toISOString() ?? null,
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "email";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.email,
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Ask the users to reset their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to reset their password after the migration, as the old password will no longer work.

      It would be best to notify your users about this change and put a notice on your login page to request a password reset.

    Google & GitHub

    Google & GitHub data migration functions
    main.wasp
    api migrateGoogle {
    httpRoute: (GET, "/migrate-google"),
    fn: import { migrateGoogleHandler } from "@src/migrateToNewAuth",
    entities: []
    }

    api migrateGithub {
    httpRoute: (GET, "/migrate-github"),
    fn: import { migrateGithubHandler } from "@src/migrateToNewAuth",
    entities: []
    }
    src/migrateToNewAuth.ts
    import { prisma } from "wasp/server";
    import { MigrateGoogle, MigrateGithub } from "wasp/server/api";

    export const migrateGoogleHandler: MigrateGoogle =
    async (_req, res) => {
    const result = await createSocialLoginMigration("google");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    export const migrateGithubHandler: MigrateGithub =
    async (_req, res) => {
    const result = await createSocialLoginMigration("github");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    async function createSocialLoginMigration(
    providerName: "google" | "github"
    ): Promise<{
    numUsersAlreadyMigrated: number;
    numUsersNotUsingThisAuthMethod: number;
    numUsersMigratedSuccessfully: number;
    }> {
    const users = await prisma.user.findMany({
    include: {
    auth: true,
    externalAuthAssociations: true,
    },
    });

    const result = {
    numUsersAlreadyMigrated: 0,
    numUsersNotUsingThisAuthMethod: 0,
    numUsersMigratedSuccessfully: 0,
    };

    for (const user of users) {
    if (user.auth) {
    result.numUsersAlreadyMigrated++;
    console.log("Skipping user (already migrated) with id:", user.id);
    continue;
    }

    const provider = user.externalAuthAssociations.find(
    (provider) => provider.provider === providerName
    );

    if (!provider) {
    result.numUsersNotUsingThisAuthMethod++;
    console.log(`Skipping user (not using ${providerName} auth) with id:`, user.id);
    continue;
    }

    await prisma.auth.create({
    data: {
    identities: {
    create: {
    providerName,
    providerUserId: provider.providerId,
    providerData: JSON.stringify({}),
    },
    },
    user: {
    connect: {
    id: user.id,
    },
    },
    },
    });
    result.numUsersMigratedSuccessfully++;
    }

    return result;
    }
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/client-config.html b/docs/0.12.0/project/client-config.html index 1ffe56ac89..88c4e9400b 100644 --- a/docs/0.12.0/project/client-config.html +++ b/docs/0.12.0/project/client-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -35,7 +35,7 @@ renders a custom layout:

    src/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return (
    <Provider store={store}>
    <Layout>{children}</Layout>
    </Provider>
    )
    }

    function Layout({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }
  • setupFn: ExtImport

    You can use this function to perform any custom setup (e.g., setting up client-side periodic jobs).

    src/myClientSetupCode.js
    export default async function mySetupFunction() {
    // Run some code
    }
  • baseDir: String

    If you need to serve the client from a subdirectory, you can use the baseDir option.

    If you set baseDir to /my-app for example, that will make Wasp set the basename prop of the Router to /my-app. It will also set the base option of the Vite config to /my-app.

    This means that if you serve your app from https://example.com/my-app, the router will work correctly, and all the assets will be served from https://example.com/my-app.

    Setting the correct env variable

    If you set the baseDir option, make sure that the WASP_WEB_CLIENT_URL env variable also includes that base directory.

    For example, if you are serving your app from https://example.com/my-app, the WASP_WEB_CLIENT_URL should be also set to https://example.com/my-app, and not just https://example.com.

  • - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/css-frameworks.html b/docs/0.12.0/project/css-frameworks.html index 4473dab54a..f4e3b8a4ea 100644 --- a/docs/0.12.0/project/css-frameworks.html +++ b/docs/0.12.0/project/css-frameworks.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── package.json
    ├── src
    │   ├── Main.css
    │   ├── MainPage.jsx
    │   ├── vite-env.d.ts
    │   └── waspLogo.png
    ├── public
    ├── tsconfig.json
    ├── vite.config.ts
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      const { resolveProjectPath } = require('wasp/dev')

      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [resolveProjectPath('./src/**/*.{js,jsx,ts,tsx}')],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, install them as npm development dependencies and add them to the plugins list in your tailwind.config.cjs file:

    npm install -D @tailwindcss/forms
    npm install -D @tailwindcss/typography

    and also

    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/custom-vite-config.html b/docs/0.12.0/project/custom-vite-config.html index e27cd3f530..0d1b3a00c9 100644 --- a/docs/0.12.0/project/custom-vite-config.html +++ b/docs/0.12.0/project/custom-vite-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Custom Vite Config

    Wasp uses Vite to serve the client during development and bundling it for production. If you want to customize the Vite config, you can do that by editing the vite.config.ts file in your project root directory.

    Wasp will use your config and merge it with the default Wasp's Vite config.

    Vite config customization can be useful for things like:

    • Adding custom Vite plugins.
    • Customising the dev server.
    • Customising the build process.

    Be careful with making changes to the Vite config, as it can break the Wasp's client build process. Check out the default Vite config here to see what you can change.

    Examples

    Below are some examples of how you can customize the Vite config.

    Changing the Dev Server Behaviour

    If you want to stop Vite from opening the browser automatically when you run wasp start, you can do that by customizing the open option.

    vite.config.js
    export default {
    server: {
    open: false,
    },
    }

    Custom Dev Server Port

    You have access to all of the Vite dev server options in your custom Vite config. You can change the dev server port by setting the port option.

    vite.config.js
    export default {
    server: {
    port: 4000,
    },
    }
    .env.server
    WASP_WEB_CLIENT_URL=http://localhost:4000
    Changing the dev server port

    ⚠️ Be careful when changing the dev server port, you'll need to update the WASP_WEB_CLIENT_URL env var in your .env.server file.

    Customising the Base Path

    If you, for example, want to serve the client from a different path than /, you can do that by customizing the base option.

    vite.config.js
    export default {
    base: '/my-app/',
    }
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/customizing-app.html b/docs/0.12.0/project/customizing-app.html index e5e5efbcf3..dd58f57bc4 100644 --- a/docs/0.12.0/project/customizing-app.html +++ b/docs/0.12.0/project/customizing-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    Customizing the App

    Each Wasp project can have only one app type declaration. It is used to configure your app and its components.

    app todoApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ]
    }

    We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the API Reference.

    Changing the App Title

    You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the title field of your app declaration:

    app myApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "BookFace"
    }

    Adding Additional Lines to the Head

    If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the head field of your app declaration.

    An example of adding extra style sheets and scripts:

    app myApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "My App",
    head: [ // optional
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
    "<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
    ]
    }

    API Reference

    app todoApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ],
    auth: {
    // ...
    },
    client: {
    // ...
    },
    server: {
    // ...
    },
    db: {
    // ...
    },
    emailSender: {
    // ...
    },
    webSocket: {
    // ...
    }
    }

    The app declaration has the following fields:

    • wasp: dict required Wasp compiler configuration. It is a dictionary with a single field:

      • version: string required

        The version specifies which versions of Wasp are compatible with the app. It should contain a valid SemVer range

        info

        For now, the version field only supports caret ranges (i.e., ^x.y.z). Support for the full specification will come in a future version of Wasp

    • title: string required

      Title of your app. It will appear in the browser tab, next to the favicon.

    • head: [string]

      List of additional lines (e.g. <link> or <script> tags) to be included in the <head> of your HTML document.

    The rest of the fields are covered in dedicated sections of the docs:

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/dependencies.html b/docs/0.12.0/project/dependencies.html index 1c709d6c36..8c15ee0234 100644 --- a/docs/0.12.0/project/dependencies.html +++ b/docs/0.12.0/project/dependencies.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    Dependencies

    In a Wasp project, dependencies are defined in a standard way for JavaScript projects: using the package.json file, located at the root of your project. You can list your dependencies under the dependencies or devDependencies fields.

    Adding a New Dependency

    To add a new package, like date-fns (a great date handling library), you use npm:

    npm install date-fns

    This command will add the package in the dependencies section of your package.json file.

    You will notice that there are some other packages in the dependencies section, like react and wasp. These are the packages that Wasp uses internally, and you should not modify or remove them.

    Using Packages that are Already Used by Wasp Internally

    In the current version of Wasp, if Wasp is already internally using a certain dependency (e.g. React) with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version.

    If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp dictates exact versions of certain packages, so for example you can't choose the version of React you want to use.

    note

    We are currently working on a restructuring that will solve this and some other quirks: check issue #734 to follow our progress.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/env-vars.html b/docs/0.12.0/project/env-vars.html index 06e7113c77..b995c1d153 100644 --- a/docs/0.12.0/project/env-vars.html +++ b/docs/0.12.0/project/env-vars.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Env Variables

    Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.

    For instance, during development, you may want your project to connect to a local development database running on your machine, but in production, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.

    While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.

    In Wasp, you can use environment variables in both the client and the server code.

    Client Env Vars

    Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    To enable Wasp to pick them up, client environment variables must be prefixed with REACT_APP_, for example: REACT_APP_SOME_VAR_NAME=....

    You can read them from the client code like this:

    src/App.js
    console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)

    Check below on how to define them.

    Server Env Vars

    In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as SOME_VAR_NAME=....

    You can read them in the server code like this:

    console.log(process.env.SOME_VAR_NAME)

    Check below on how to define them.

    Defining Env Vars in Development

    During development, there are two ways to provide env vars to your Wasp project:

    1. Using .env files. (recommended)
    2. Using shell. (useful for overrides)

    1. Using .env (dotenv) Files

    Env vars usage in development

    This is the recommended method for providing env vars to your Wasp project during development.

    In the root of your Wasp project you can create two distinct files:

    • .env.server for env vars that will be provided to the server.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.server
      DATABASE_URL=postgresql://localhost:5432
      SOME_VAR_NAME=somevalue
    • .env.client for env vars that will be provided to the client.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.client
      REACT_APP_SOME_VAR_NAME=somevalue

    These files should not be committed to version control, and they are already ignored by default in the .gitignore file that comes with Wasp.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, or by defining them at the start of a command:

    SOME_VAR_NAME=SOMEVALUE wasp start

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    You should provide them to the build command, for example:

    REACT_APP_SOME_VAR_NAME=somevalue npm run build
    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME with the value you provided. This is done during the build process, so the value is embedded into the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/server-config.html b/docs/0.12.0/project/server-config.html index 6afc5840be..b8f00c350e 100644 --- a/docs/0.12.0/project/server-config.html +++ b/docs/0.12.0/project/server-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ExtImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server logic.

      src/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/starter-templates.html b/docs/0.12.0/project/starter-templates.html index 76485e0168..783a1e629e 100644 --- a/docs/0.12.0/project/starter-templates.html +++ b/docs/0.12.0/project/starter-templates.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    Simple starter template with a single page.
    [2] todo-ts
    Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
    [3] saas
    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
    [4] embeddings
    Comes with code for generating vector embeddings and performing vector similarity search.
    [5] ai-generated
    🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
    ▸ 1

    🐝 --- Creating your project from the "basic" template... -------------------------

    Created new Wasp app in ./MyFirstProject directory!

    To run your new app, do:
    cd MyFirstProject
    wasp db start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    OpenSaaS.sh template

    SaaS Template

    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.

    Features: Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    Todo App w/ Typescript

    A simple Todo App with Typescript and Fullstack Type Safety.

    Features: Auth (username/password), Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts

    AI Generated Starter 🤖

    Using the same tech as used on https://usemage.ai/, Wasp generates your custom starter template based on your project description. It will automatically generate your data model, auth, queries, actions and React pages.

    You will need to provide your own OpenAI API key to be able to use this template.

    Features: Generated using OpenAI's GPT models, Auth (username/password), Queries, Actions, Pages, Fullstack Type Safety

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/static-assets.html b/docs/0.12.0/project/static-assets.html index 45e19d3532..58280da4ee 100644 --- a/docs/0.12.0/project/static-assets.html +++ b/docs/0.12.0/project/static-assets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Static Asset Handling

    Importing an Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in the public directory at the root of your project:

    .
    └── public
    ├── favicon.ico
    └── robots.txt

    Assets in this directory will be served at root path / during development and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path
      • for example, public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/project/testing.html b/docs/0.12.0/project/testing.html index 837c446b35..796e341070 100644 --- a/docs/0.12.0/project/testing.html +++ b/docs/0.12.0/project/testing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a realtime UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "wasp/client/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "wasp/client/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import { getTasks } from "wasp/client/operations";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "wasp/client";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/Todo.jsx
    import { useQuery, getTasks } from "wasp/client/operations";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import { getTasks } from "wasp/client/operations";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/Todo.jsx
    import { api } from "wasp/client/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    - - + + \ No newline at end of file diff --git a/docs/0.12.0/quick-start.html b/docs/0.12.0/quick-start.html index 77eab3077a..a0846598b6 100644 --- a/docs/0.12.0/quick-start.html +++ b/docs/0.12.0/quick-start.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Quick Start

    Installation

    Welcome, new Waspeteer 🐝!

    Let's create and run our first Wasp app in 3 short steps:

    1. To install Wasp on Linux / OSX / WSL (Windows), open your terminal and run:

      curl -sSL https://get.wasp-lang.dev/installer.sh | sh

      ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    2. Then, create a new app by running:

      wasp new
    3. Finally, run the app:

      cd <my-project-name>
      wasp start

    That's it 🎉 You have successfully created and served a new full-stack web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong with the installation, or if you have additional questions.

    Want an even faster start?

    Try out Wasp AI 🤖 to generate a new Wasp app in minutes just from a title and short description!

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser without any setup by running our Wasp Template for Gitpod

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. A version of Node.js must be >= 18.

    If you need it, we recommend using nvm for managing your Node.js installation version(s).

    A quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 20

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 20

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh
    Running Wasp on Mac with Mx chip (arm64)

    Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)? Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install Rosetta on your Mac if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal

    softwareupdate --install-rosetta

    Once Rosetta is installed, you should be able to run Wasp without any issues.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/telemetry.html b/docs/0.12.0/telemetry.html index 05d6b7c8c0..6ae8f541d0 100644 --- a/docs/0.12.0/telemetry.html +++ b/docs/0.12.0/telemetry.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.12.0

    Telemetry

    Overview

    The term telemetry refers to the collection of certain usage data to help improve the quality of a piece of software (in this case, Wasp).

    Our telemetry implementation is anonymized and very limited in its scope, focused on answering following questions:

    • How many people and how often: tried to install Wasp, use Wasp, have built a Wasp app, or have deployed one?
    • How many projects are created with Wasp?

    When and what is sent?

    • Information is sent via HTTPS request when wasp CLI command is invoked. Information is sent no more than twice in a period of 12 hours (sending is paused for 12 hours after last invocation, separately for wasp build command and for all other commands). Exact information as it is sent:
      {
      // Randomly generated, non-identifiable UUID representing a user.
      "distinct_id": "bf3fa7a8-1c11-4f82-9542-ec1a2d28786b",
      // Non-identifiable hash representing a project.
      "project_hash": "6d7e561d62b955d1",
      // True if command was `wasp build`, false otherwise.
      "is_build": true,
      // Captures `wasp deploy ...` args, but only those from the limited, pre-defined list of keywords.
      // Those are "fly", "setup", "create-db", "deploy" and "cmd". Everything else is ommited.
      "deploy_cmd_args": "fly;deploy",
      "wasp_version": "0.1.9.1",
      "os": "linux",
      // "CI" if running on CI, and whatever is the content of "WASP_TELEMETRY_CONTEXT" env var.
      // We use this to track when execution is happening in some special context, like on Gitpod, Replit or similar.
      "context": "CI"
      }
    • Information is also sent once via HTTPS request when wasp is installed via install.sh script. Exact information as it is sent:
      {
      // Randomly generated id.
      "distinct_id": "274701613078193779564259",
      "os": "linux"
      }

    Opting out

    You sharing the telemetry data with us means a lot to us, since it helps us understand how popular Wasp is, how it is being used, how the changes we are doing affect usage, how many new vs old users there are, and just in general how Wasp is doing. We look at these numbers every morning and they drive us to make Wasp better.

    However, if you wish to opt-out of telemetry, we understand! You can do so by setting the WASP_TELEMETRY_DISABLE environment variable to any value, e.g.:

    export WASP_TELEMETRY_DISABLE=1

    Future plans

    We don't have this implemented yet, but the next step will be to make telemetry go in two directions -> instead of just sending usage data to us, it will also at the same time check for any messages from our side (e.g. notification about new version of Wasp, or a security notice). Link to corresponding github issue.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/actions.html b/docs/0.12.0/tutorial/actions.html index 03217d4eda..f6dc6c6d55 100644 --- a/docs/0.12.0/tutorial/actions.html +++ b/docs/0.12.0/tutorial/actions.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    6. Modifying Data

    In the previous section, we learned about using Queries to fetch data and only briefly mentioned that Actions can be used to update the database. Let's learn more about Actions so we can add and update tasks in the database.

    We have to create:

    1. A Wasp Action that creates a new task.
    2. A React form that calls that Action when the user creates a task.

    Creating a New Action

    Creating an Action is very similar to creating a Query.

    Declaring an Action

    We must first declare the Action in main.wasp:

    main.wasp
    // ...

    action createTask {
    fn: import { createTask } from "@src/actions",
    entities: [Task]
    }

    Implementing an Action

    Let's now define a function for our createTask Action:

    src/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src directory.

    Invoking the Action on the Client

    Start by defining a form for creating new tasks.

    src/MainPage.jsx
    import { 
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    // ... MainPage, TaskView, TaskList ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike Queries, you can call Actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.

    All that's left now is adding this form to the page component:

    src/MainPage.jsx
    import {
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    // ... TaskView, TaskList, NewTaskForm ...

    Great work!

    You now have a form for creating new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser. You'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! Wasp handles these automatic updates under the hood.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp keeps all your queries in sync with any changes made through Actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done.

    We'll create a new Action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an Action named updateTask that receives the task's id and its isDone status. You can see our implementation below.

    Solution

    Declaring the Action in main.wasp:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@src/actions",
    entities: [Task]
    }

    Implementing the Action on the server:

    src/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    You can now call updateTask from the React component:

    src/MainPage.jsx
    // ...
    import {
    updateTask,
    createTask,
    getTasks,
    useQuery,
    } from 'wasp/client/operations'

    // ... MainPage ...

    const TaskView = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error) {
    window.alert('Error while updating task: ' + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ... TaskList, NewTaskForm ...

    Awesome! You can now mark this task as done.

    It's time to make one final addition to your app: supporting multiple users.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/auth.html b/docs/0.12.0/tutorial/auth.html index bcb3804e95..3745175498 100644 --- a/docs/0.12.0/tutorial/auth.html +++ b/docs/0.12.0/tutorial/auth.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    7. Adding Authentication

    Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support.

    To add users to your app, you must:

    • Create a User Entity.
    • Tell Wasp to use the Username and Password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify your Queries and Actions so users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it will create the auth related entities for you in the background. Nothing to do here!

    You must only add the User Entity to keep track of who owns which tasks.

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    Adding Auth to the Project

    Next, tell Wasp to use full-stack authentication:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "TodoApp",
    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used in a bit.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    Don't forget to update the database schema by running:

    wasp db migrate-dev

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage"
    }

    Great, Wasp now knows these pages exist!

    Here's the React code for the pages you've just imported:

    src/LoginPage.jsx
    import { Link } from 'react-router-dom'
    import { LoginForm } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <LoginForm />
    <br />
    <span>
    I don't have an account yet (<Link to="/signup">go to signup</Link>).
    </span>
    </div>
    )
    }

    The signup page is very similar to the login page:

    src/SignupPage.jsx
    import { Link } from 'react-router-dom'
    import { SignupForm } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <SignupForm />
    <br />
    <span>
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </div>
    )
    }

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import { MainPage } from "@src/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/MainPage.jsx
    export const MainPage = ({ user }) => {
    // Do something with the user
    // ...
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    You'll notice that we now have a User entity in the database alongside the Task entity.

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    You might notice some extra Prisma models like Auth, AuthIdentity and Session that Wasp created for us. You don't need to care about these right now, but if you are curious, you can read more about them here.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    tasks Task[]
    psl=}

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User? @relation(fields: [userId], references: [id])
    userId Int?
    psl=}

    // ...

    As always, you must migrate the database after changing the Entities:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database.

    This isn't recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional.

    Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/queries.js
    import { HttpError } from 'wasp/server'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
    })
    }
    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/MainPage.jsx
    // ...
    import { logout } from 'wasp/client/auth'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/create.html b/docs/0.12.0/tutorial/create.html index 165a4cdefb..61e0ed7fa6 100644 --- a/docs/0.12.0/tutorial/create.html +++ b/docs/0.12.0/tutorial/create.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start
    note

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder page:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/entities.html b/docs/0.12.0/tutorial/entities.html index 7db245dbc5..e99bd3a37f 100644 --- a/docs/0.12.0/tutorial/entities.html +++ b/docs/0.12.0/tutorial/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:

    main.wasp
    // ...

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}
    note

    Wasp uses Prisma as a way to talk to the database. You define entities by defining Prisma models using the Prisma Schema Language (PSL) between the {=psl psl=} tags.

    Read more in the Entities section of the docs.

    To update the database schema to include this entity, stop the wasp start process, if it's running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/pages.html b/docs/0.12.0/tutorial/pages.html index 034172184f..d0c4e2d7ca 100644 --- a/docs/0.12.0/tutorial/pages.html +++ b/docs/0.12.0/tutorial/pages.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the named export from src/MainPage.tsx.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    export const MainPage = () => {
    // ...
    }

    This is a regular functional React component. It also uses the CSS file and a logo image that sit next to it in the src folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    tip

    wasp start automatically picks up the changes you make and restarts the app, so keep it running in the background.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import { HelloPage } from "@src/HelloPage"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/HelloPage.tsx and pass the URL parameter the same way as in React Router:

    src/HelloPage.jsx
    export const HelloPage = (props) =>  {
    return <div>Here's {props.match.params.name}!</div>
    }

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Now that you've seen how Wasp deals with Routes and Pages, it's finally time to build the Todo app.

    Start by cleaning up the starter project and removing unnecessary code and files.

    First, remove most of the code from the MainPage component:

    src/MainPage.jsx
    export const MainPage = () => {
    return <div>Hello world!</div>
    }

    At this point, the main page should look like this:

    Todo App - Hello World

    You can now delete redundant files: src/Main.css, src/waspLogo.png, and src/HelloPage.tsx (we won't need this page for the rest of the tutorial).

    Since src/HelloPage.tsx no longer exists, remove its route and page declarations from the main.wasp file.

    Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.12.0"
    },
    title: "TodoApp"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@src/MainPage"
    }

    Excellent work!

    You now have a basic understanding of Wasp and are ready to start building your TodoApp. We'll implement the app's core features in the following sections.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/project-structure.html b/docs/0.12.0/tutorial/project-structure.html index 7206d89c93..9bf6b3cd07 100644 --- a/docs/0.12.0/tutorial/project-structure.html +++ b/docs/0.12.0/tutorial/project-structure.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ We'll keep it simple by placing everything in the root src directory.

    Many other files (e.g., tsconfig.json, vite-env.d.ts, .wasproot, etc.) help Wasp and the IDE improve your development experience with autocompletion, IntelliSense, and error reporting.

    The vite.config.ts file is used to configure Vite, Wasp's build tool of choice. We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in custom Vite config docs.

    There's no need to spend more time discussing all the helper files. They'll silently do their job in the background and let you focus on building your app.

    TypeScript Support

    Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla JavaScript and TypeScript.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's take a closer look at main.wasp

    main.wasp

    main.wasp is your app's definition file. It defines the app's central components and helps Wasp to do a lot of the legwork for you.

    The file is a list of declarations. Each declaration defines a part of your app.

    The default main.wasp file generated with wasp new on the previous page looks like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.12.0" // Pins the version of Wasp to use.
    },
    title: "TodoApp" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that gets rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/tutorial/queries.html b/docs/0.12.0/tutorial/queries.html index 643023e30b..9676e524bd 100644 --- a/docs/0.12.0/tutorial/queries.html +++ b/docs/0.12.0/tutorial/queries.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them!

    The primary way of working with Entities in Wasp is with Queries and Actions, collectively known as Operations.

    Queries are used to read an entity, while Actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a Query.

    To list the tasks, you must:

    1. Create a Query that fetches the tasks from the database.
    2. Update the MainPage.tsx to use that Query and display the results.

    Defining the Query

    We'll create a new Query called getTasks. We'll need to declare the Query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // The path `@src/queries` resolves to `src/queries.js`.
    // No need to specify an extension.
    fn: import { getTasks } from "@src/queries",
    // Tell Wasp that this query reads from the `Task` entity. Wasp will
    // automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'asc' },
    })
    }

    Query function parameters:

    • args: object

      The arguments the caller passes to the Query.

    • context

      An object with extra information injected by Wasp. Its type depends on the Query declaration.

    Since the Query declaration in main.wasp says that the getTasks Query uses Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and Actions are NodeJS functions executed on the server.

    Invoking the Query On the Frontend

    While we implement Queries on the server, Wasp generates client-side functions that automatically take care of serialization, network calls, and cache invalidation, allowing you to call the server code like it's a regular function.

    This makes it easy for us to use the getTasks Query we just created in our React component:

    src/MainPage.jsx
    import { getTasks, useQuery } from 'wasp/client/operations'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const TaskView = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <TaskView task={task} key={idx} />
    ))}
    </div>
    )
    }

    Most of this code is regular React, the only exception being the special wasp imports:

    We could have called the Query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the Query changes. Remember that Wasp automatically refreshes Queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/vision.html b/docs/0.12.0/vision.html index 23da0e5d8b..b9cffac1be 100644 --- a/docs/0.12.0/vision.html +++ b/docs/0.12.0/vision.html @@ -19,8 +19,8 @@ - - + +
    @@ -33,7 +33,7 @@ They can be used inline (mixed with Wasp code) or provided via external files.
  • Has hatches (escape mechanisms) that allow you to customize your web app in all the right places, but remain hidden until you need them.
  • Entity (data model) is a first-class citizen - defined via custom Wasp syntax and it integrates very closely with the rest of the features, serving as one of the central concepts around which everything is built.
  • Out of the box support for CRUD UI based on the Entities, to get you quickly going, but also customizable to some level.
  • "Smart" operations (queries and actions) that in most cases automatically figure out when to update, and if not it is easy to define custom logic to compensate for that. User worries about client-server gap as little as possible.
  • Support, directly in Wasp, for declaratively defining simple components and operations.
  • Besides Wasp as a programming language, there will also be a visual builder that generates/edits Wasp code, allowing non-developers to participate in development. Since Wasp is declarative, we imagine such builder to naturally follow from Wasp language.
  • Server side rendering, caching, packaging, security, ... -> all those are taken care of by Wasp. You tell Wasp what you want, and Wasp figures out how to do it.
  • As simple deployment to production/staging as it gets.
  • While it comes with the official implementation(s), Wasp language will not be coupled with the single implementation. Others can provide implementations that compile to different web app stacks.
  • - - + + \ No newline at end of file diff --git a/docs/0.12.0/wasp-ai/creating-new-app.html b/docs/0.12.0/wasp-ai/creating-new-app.html index 5ec43012cf..774f1cecce 100644 --- a/docs/0.12.0/wasp-ai/creating-new-app.html +++ b/docs/0.12.0/wasp-ai/creating-new-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.12.0

    Creating New App with AI

    Wasp comes with its own AI: Wasp AI, aka Mage (Magic web App GEnerator).

    Wasp AI allows you to create a new Wasp app from only a title and a short description (using GPT in the background)!

    There are two main ways to create a new Wasp app with Wasp AI:

    1. Free, open-source online app usemage.ai.
    2. Running wasp new on your machine and picking AI generation. For this you need to provide your own OpenAI API keys, but it allows for more flexibility (choosing GPT models).

    They both use the same logic in the background, so both approaches are equally "smart", the difference is just in the UI / settings.

    info

    Wasp AI is an experimental feature. Apps that Wasp AI generates can have mistakes (proportional to their complexity), but even then they can often serve as a great starting point (once you fix the mistakes) or an interesting way to explore how to implement stuff in Wasp.

    usemage.ai

    1. Describe your app 2. Pick the color 3. Generate your app 🚀

    Mage is an open-source app with which you can create new Wasp apps from just a short title and description.

    It is completely free for you - it uses our OpenAI API keys and we take on the costs.

    Once you provide an app title, app description, and choose some basic settings, your new Wasp app will be created for you in a matter of minutes and you will be able to download it to your machine and keep working on it!

    If you want to know more, check this blog post for more details on how Mage works, or this blog post for a high-level overview of how we implemented it.

    Wasp Cli

    You can create a new Wasp app using Wasp AI by running wasp new in your terminal and picking AI generation.

    If you don't have them set yet, wasp will ask you to provide (via ENV vars) your OpenAI API keys (which it will use to query GPT).

    Then, after providing a title and description for your Wasp app, the new app will be generated on your disk!

    wasp-cli-ai-input wasp-cli-ai-generation

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/wasp-ai/developing-existing-app.html b/docs/0.12.0/wasp-ai/developing-existing-app.html index 62bf102be9..b9d875e5a2 100644 --- a/docs/0.12.0/wasp-ai/developing-existing-app.html +++ b/docs/0.12.0/wasp-ai/developing-existing-app.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.12.0

    Developing Existing App with AI

    While Wasp AI doesn't at the moment offer any additional help for developing your Wasp app with AI beyond initial generation, this is something we are exploring actively.

    In the meantime, while waiting for Wasp AI to add support for this, we suggest checking out aider, which is an AI pair programming tool in your terminal. This is a third-party tool, not affiliated with Wasp in any way, but we and some of Wasp users have found that it can be helpful when working on Wasp apps.

    - - + + \ No newline at end of file diff --git a/docs/0.12.0/writingguide.html b/docs/0.12.0/writingguide.html index fecab7c3fc..142601ca02 100644 --- a/docs/0.12.0/writingguide.html +++ b/docs/0.12.0/writingguide.html @@ -19,8 +19,8 @@ - - + +
    @@ -53,7 +53,7 @@ Many of our titles are currently in title-case, we should start phasing those out.
  • Use the Oxford comma (e.g., "a, b, and c" instead of "a, b and c"). Why the Oxford comma is important
  • Content and Communication

    • Excellence comes from iteration. First drafts are always bad, but writing them is a vital part of the process. It's extremely difficult to avoid the slow progression of Bad -> OK -> Good -> Great -> Inspiring -> Transcendent.
    • Only wait until something is "Good" before publishing. Vue's guide originally says: "The community will help you push it further down the chain." We don't yet have that luxury, as our community isn't large enough. Still, we can't afford to invest too much time into the docs, so "Good" will have to do for now.

    Processes

    • Ideally, you should write the docs before you implement the feature. This will help you see the feature from the user's perspective and better spot the API's deficiencies and improvement potential. If something is difficult to explain, it's most likely difficult to understand. If it is difficult to understand, there might be a better way of designing it.
    • Try not to get defensive when receiving feedback. Our writing can be very personal to us, but if we get upset with the people who help us improve it, they will either stop giving feedback or start limiting the kind of feedback they give.
    • Proofread your work before showing it to others (and use Grammarly). If you show someone work with many spelling/grammar mistakes, you'll get feedback about spelling grammar/mistakes instead of more valuable notes about whether the writing is achieving your goals.
    • When you ask people for feedback, tell reviewers:
      • What you're trying to do.
      • What your fears are.
      • Which balances you're trying to strike.
    • Do your best to come up with a good and straighforward way to say something. Again, this will help the reviewer focus on high-level issues instead of rephrasing your sentences.
    • Read and correct your text several times before submitting it (preferably with some time between the readings). This is similar to proofreading but has more to do with content and communication style than grammar. A time offset is beneficial because it removes the text from your short-term memory, helping you view it more objectively.
    • It's OK to ask AI to improve your text. Just make sure to check it and correct it. You should always sign off on the last version.
    • When someone reports a problem, there is almost always a problem, even if the solution they proposed isn't quite right. Keep asking follow-up questions to learn more.
    • People need to feel safe asking questions when contributing/reviewing content. Here's how you can do that:
      • Thank people for their contributions/reviews, even if you're feeling grumpy. For example:
        • "Great question!"
        • "Thanks for taking the time to explain. 🙂"
        • "This is actually intentional, but thanks for taking the time to contribute. 😊"
      • Listen to what people are saying and mirror if you're not sure you're understanding correctly. This can help validate people's feelings and experiences while also understanding if you're understanding them correctly.
      • Use a lot of positive and empathetic emojis. It's always better to seem a little strange than mean or impatient. This primarily applies to Wasp team members speaking to outside contributors. Since most of the core team knows each other pretty well, there's no need to go overboard with the emojis and pleasantries.
      • Kindly communicate rules/boundaries. If someone behaves in a way that's abusive/inappropriate, respond only with kindness and maturity, but also make it clear that this behavior is not acceptable and what will happen (according to the code of conduct) if they continue behaving poorly.
    • All docs must go through the review cycle, preferably with more than a single reviewer. Different people focus on different things. Some of us are great at coming up with examples, others easily come up with analogies and explain complex topics, some have a clear and concise writing style, etc. Therefore, try to get at least two or three people to review your document.

    Possible improvements

    • Some parts of our docs don't follow all the guidelines outlined in this document. There's no need to start fixing all the issues right away. We can slowly improve the docs as we edit them.
    • We've discussed having a git repo with all the example code in the docs. This should make copying, pasting, testing, and maintaining code snippets easier.
    - - + + \ No newline at end of file diff --git a/docs/0.13.0.html b/docs/0.13.0.html index eac644e8b3..01a1c8b272 100644 --- a/docs/0.13.0.html +++ b/docs/0.13.0.html @@ -19,8 +19,8 @@ - - + +
    @@ -30,7 +30,7 @@ which are in their essence a Node.js functions that execute on server and can, thanks to Wasp, very easily be called from the client.

    First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up":

    main.wasp
    // Queries have automatic cache invalidation and are type-safe.
    query getRecipes {
    fn: import { getRecipes } from "@src/recipe/operations.ts",
    entities: [Recipe],
    }

    // Actions are type-safe and can be used to perform side-effects.
    action addRecipe {
    fn: import { addRecipe } from "@src/recipe/operations.ts",
    entities: [Recipe],
    }

    ... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):

    src/recipe/operations.ts
    // Wasp generates the types for you.
    import { type GetRecipes } from "wasp/server/operations";
    import { type Recipe } from "wasp/entities";

    export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
    return context.entities.Recipe.findMany( // Prisma query
    { where: { user: { id: context.user.id } } }
    );
    };

    export const addRecipe ...

    Now we can very easily use these in our React components!

    For the end, let's create a home page of our app.

    First we define it in main.wasp:

    main.wasp
    ...

    route HomeRoute { path: "/", to: HomePage }
    page HomePage {
    component: import { HomePage } from "@src/pages/HomePage",
    authRequired: true // Will send user to /login if not authenticated.
    }

    and then implement it as a React component in JS/TS (that calls the Operations we previously defined):

    src/pages/HomePage.tsx
    import { useQuery, getRecipes } from "wasp/client/operations";
    import { type User } from "wasp/entities";

    export function HomePage({ user }: { user: User }) {
    // Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
    const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here!

    if (isLoading) {
    return <div>Loading...</div>;
    }

    return (
    <div>
    <h1>Recipes</h1>
    <ul>
    {recipes ? recipes.map((recipe) => (
    <li key={recipe.id}>
    <div>{recipe.title}</div>
    <div>{recipe.description}</div>
    </li>
    )) : 'No recipes defined yet!'}
    </ul>
    </div>
    );
    }

    And voila! We are listing all the recipes in our app 🎉

    This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the Todo app tutorial.

    note

    Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.

    When to use Wasp

    Wasp is addressing the same core problems that typical web app frameworks are addressing, and it in big part looks, swims and quacks like a web app framework.

    Best used for

    • building full-stack web apps (like e.g. Airbnb or Asana)
    • quickly starting a web app with industry best practices
    • to be used alongside modern web dev stack (currently supported React and Node)

    Avoid using Wasp for

    • building static/presentational websites
    • to be used as a no-code solution
    • to be a solve-it-all tool in a single language

    Wasp is a DSL

    note

    You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below.

    Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a simple programming language that understands your code and can do a lot of things for you.

    Wasp is a programming language, but a specific kind: it is specialized for a single purpose: building modern web applications. We call such languages DSLs (Domain Specific Language).

    Other examples of DSLs that are often used today are e.g. SQL for databases and HTML for web page layouts. The main advantage and reason why DSLs exist is that they need to do only one task (e.g. database queries) so they can do it well and provide the best possible experience for the developer.

    The same idea stands behind Wasp - a language that will allow developers to build modern web applications with 10x less code and less stack-specific knowledge.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/accessing-app-config.html b/docs/0.13.0/advanced/accessing-app-config.html index bfac7132ff..098ac01055 100644 --- a/docs/0.13.0/advanced/accessing-app-config.html +++ b/docs/0.13.0/advanced/accessing-app-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -37,7 +37,7 @@ Wasp automatically sets it during development when you run wasp start.
    In production, it should contain the value of your server's URL as the user's browser sees it (i.e., with the DNS and proxies considered).

    You can access it like this:

    import { config } from 'wasp/client'

    console.log(config.apiUrl)
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/apis.html b/docs/0.13.0/advanced/apis.html index c8489334e8..8df7b69de6 100644 --- a/docs/0.13.0/advanced/apis.html +++ b/docs/0.13.0/advanced/apis.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Custom HTTP API Endpoints

    In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

    How to Create an API

    APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

    To create a Wasp API, you must:

    1. Declare the API in Wasp using the api declaration
    2. Define the API's NodeJS implementation

    After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

    Declaring the API in Wasp

    First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

    main.wasp
    // ...

    api fooBar { // APIs and their implementations don't need to (but can) have the same name.
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar")
    }

    Read more about the supported fields in the API Reference.

    Defining the API's NodeJS Implementation

    After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

    1. req: Express Request object
    2. res: Express Response object
    3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
    src/apis.js
    export const fooBar = (req, res, context) => {
    res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
    res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
    };

    Using the API

    Using the API externally

    To use the API externally, you simply call the endpoint using the method and path you used.

    For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

    Using the API from the Client

    To use the API from your client, including with auth support, you can import the Axios wrapper from wasp/client/api and invoke a call. For example:

    src/pages/SomePage.jsx
    import React, { useEffect } from "react";
    import { api } from "wasp/client/api";

    async function fetchCustomRoute() {
    const res = await api.get("/foo/bar");
    console.log(res.data);
    }

    export const Foo = () => {
    useEffect(() => {
    fetchCustomRoute();
    }, []);

    return <>// ...</>;
    };

    Making Sure CORS Works

    APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

    You can do this by defining custom middleware for your APIs in the Wasp file.

    For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

    main.wasp
    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo"
    }

    And then in the implementation file:

    src/apis.js
    export const apiMiddleware = (config) => {
    return config;
    };

    We are returning the default middleware which enables CORS for all APIs under the /foo path.

    For more information about middleware configuration, please see: Middleware Configuration

    Using Entities in APIs

    In many cases, resources used in APIs will be Entities. To use an Entity in your API, add it to the api declaration in Wasp:

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    entities: [Task],
    httpRoute: (GET, "/foo/bar")
    }

    Wasp will inject the specified Entity into the APIs context argument, giving you access to the Entity's Prisma API:

    src/apis.js
    export const fooBar = (req, res, context) => {
    res.json({ count: await context.entities.Task.count() });
    };

    The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

    API Reference

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar"),
    entities: [Task],
    auth: true,
    middlewareConfigFn: import { apiMiddleware } from "@src/apis"
    }

    The api declaration has the following fields:

    • fn: ExtImport required

      The import statement of the APIs NodeJs implementation.

    • httpRoute: (HttpMethod, string) required

      The HTTP (method, path) pair, where the method can be one of:

      • ALL, GET, POST, PUT or DELETE
      • and path is an Express path string.
    • entities: [Entity]

      A list of entities you wish to use inside your API. You can read more about it here.

    • auth: bool

      If auth is enabled, this will default to true and provide a context.user object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to false.

    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function for this API. See more in middleware section of the docs.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/deployment/cli.html b/docs/0.13.0/advanced/deployment/cli.html index a9480aeae5..1f2ea0b61d 100644 --- a/docs/0.13.0/advanced/deployment/cli.html +++ b/docs/0.13.0/advanced/deployment/cli.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Deploying with the Wasp CLI

    Wasp CLI can deploy your full-stack application with only a single command. The command automates the manual deployment process and is the recommended way of deploying Wasp apps.

    Supported Providers

    Wasp supports automated deployment to the following providers:

    • Fly.io - they offer 5$ free credit each month
    • Railway (coming soon, track it here #1157)

    Fly.io

    Prerequisites

    Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.

    You can add the required credit card information on the account's billing page.

    Fly.io CLI

    You will need the flyctl CLI installed on your machine before you can deploy to Fly.io.

    Deploying

    Using the Wasp CLI, you can easily deploy a new app to Fly.io with just a single command:

    wasp deploy fly launch my-wasp-app mia
    Specifying Org

    If your account is a member of more than one organization on Fly.io, you will need to specify under which one you want to execute the command. To do that, provide an additional --org <org-slug> option. You can find out the names(slugs) of your organizations by running fly orgs list.

    Please do not CTRL-C or exit your terminal while the commands are running.

    Under the covers, this runs the equivalent of the following commands:

    wasp deploy fly setup my-wasp-app mia
    wasp deploy fly create-db mia
    wasp deploy fly deploy

    The commands above use the app basename my-wasp-app and deploy it to the Miami, Florida (US) region (called mia). Read more about Fly.io regions here.

    Unique Name

    Your app name must be unique across all of Fly or deployment will fail.

    The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:

    • my-wasp-app-client
    • my-wasp-app-server
    • my-wasp-app-db

    You'll notice that Wasp creates two new files in your project root directory:

    • fly-server.toml
    • fly-client.toml

    You should include these files in your version control so that you can deploy your app with a single command in the future.

    Using a Custom Domain For Your App

    Setting up a custom domain is a three-step process:

    1. You need to add your domain to your Fly client app. You can do this by running:
    wasp deploy fly cmd --context client certs create mycoolapp.com
    Use Your Domain

    Make sure to replace mycoolapp.com with your domain in all of the commands mentioned in this section.

    This command will output the instructions to add the DNS records to your domain. It will look something like this:

    You can direct traffic to mycoolapp.com by:

    1: Adding an A record to your DNS service which reads

    A @ 66.241.1XX.154

    You can validate your ownership of mycoolapp.com by:

    2: Adding an AAAA record to your DNS service which reads:

    AAAA @ 2a09:82XX:1::1:ff40
    1. You need to add the DNS records for your domain:

      This will depend on your domain provider, but it should be a matter of adding an A record for @ and an AAAA record for @ with the values provided by the previous command.

    2. You need to set your domain as the WASP_WEB_CLIENT_URL environment variable for your server app:

    wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com

    We need to do this to keep our CORS configuration up to date.

    That's it, your app should be available at https://mycoolapp.com! 🎉

    API Reference

    launch

    launch is a convenience command that runs setup, create-db, and deploy in sequence.

    wasp deploy fly launch <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    It gives you the same result as running the following commands:

    wasp deploy fly setup <app-name> <region>
    wasp deploy fly create-db <region>
    wasp deploy fly deploy

    Environment Variables

    If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the --server-secret option:

    wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>

    setup

    setup will create your client and server apps on Fly, and add some secrets, but does not deploy them.

    wasp deploy fly setup <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    After running setup, Wasp creates two new files in your project root directory: fly-server.toml and fly-client.toml. You should include these files in your version control.

    You can edit the fly-server.toml and fly-client.toml files to further configure your Fly deployments. Wasp will use the TOML files when you run deploy.

    If you want to maintain multiple apps, you can add the --fly-toml-dir <abs-path> option to point to different directories, like "dev" or "staging".

    Execute Only Once

    You should only run setup once per app. If you run it multiple times, it will create unnecessary apps on Fly.

    create-db

    create-db will create a new database for your app.

    wasp deploy fly create-db <region>

    It accepts the following arguments:

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    Execute Only Once

    You should only run create-db once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.

    deploy

    wasp deploy fly deploy

    deploy pushes your client and server live.

    Run this command whenever you want to update your deployed app with the latest changes:

    wasp deploy fly deploy

    cmd

    If want to run arbitrary Fly commands (e.g. flyctl secrets list for your server app), here's how to do it:

    wasp deploy fly cmd secrets list --context server

    Fly.io Regions

    Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.

    Read more on Fly regions here.

    You can find the list of all available Fly regions by running:

    flyctl platform regions

    Environment Variables

    If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the secrets set command:

    wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server

    Multiple Fly Organizations

    If you have multiple organizations, you can specify a --org option. For example:

    wasp deploy fly launch my-wasp-app mia --org hive

    Building Locally

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.

    If you want to build locally, supply the --build-locally option to wasp deploy fly launch or wasp deploy fly deploy.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/deployment/manually.html b/docs/0.13.0/advanced/deployment/manually.html index bba95824e3..7d9e132ace 100644 --- a/docs/0.13.0/advanced/deployment/manually.html +++ b/docs/0.13.0/advanced/deployment/manually.html @@ -19,8 +19,8 @@ - - + +
    @@ -40,7 +40,7 @@ Set it to a random string at least 32 characters long (you can use an online generator).

  • PORT

    The server's HTTP port number. This is where the server listens for requests (default: 3001).

  • Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    While these are the general instructions on deploying the server anywhere, we also have more detailed instructions for chosen providers below, so check that out for more guidance if you are deploying to one of those providers.

    3. Deploying the Web Client (frontend)

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    The command above will build the web client and put it in the build/ directory in the web-app directory.

    Since the app's frontend is just a bunch of static files, you can deploy it to any static hosting provider.

    4. Deploying the Database

    Any PostgreSQL database will do, as long as you provide the server with the correct DATABASE_URL env var and ensure that the database is accessible from the server.

    Different Providers

    We'll cover a few different deployment providers below:

    • Fly.io (server and database)
    • Netlify (client)
    • Railway (server, client and database)
    • Heroku (server and database)

    Fly.io (server and database)

    We will show how to deploy the server and provision a database for it on Fly.io.

    We automated this process for you

    If you want to do all of the work below with one command, you can use the Wasp CLI.

    Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command.

    Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the flyctl CLI.

    note

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.

    Additionally, fly is a symlink for flyctl on most systems and they can be used interchangeably.

    Make sure you are logged in with flyctl CLI. You can check if you are logged in with flyctl auth whoami, and if you are not, you can log in with flyctl auth login.

    Set Up a Fly.io App

    info

    You need to do this only once per Wasp app.

    Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    Next, run the launch command to set up a new app and create a fly.toml file:

    flyctl launch --remote-only

    This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.

    • Say yes to Would you like to set up a PostgreSQL database now? and select Development. Fly.io will set a DATABASE_URL for you.

    • Say no to Would you like to deploy now? (and to any additional questions).

      We still need to set up several environment variables.

    What if the database setup fails?

    If your attempts to initiate a new app fail for whatever reason, then you should run flyctl apps destroy <app-name> before trying again. Fly does not allow you to create multiple apps with the same name.

    What does it look like when your DB is deployed correctly?

    When your DB is deployed correctly, you'll see it in the Fly.io dashboard:

    image

    Next, let's copy the fly.toml file up to our Wasp project dir for safekeeping.

    cp fly.toml ../../

    Next, let's add a few more environment variables:

    flyctl secrets set PORT=8080
    flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
    flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_client_will_be_deployed>
    flyctl secrets set WASP_SERVER_URL=<url_of_where_server_will_be_deployed>
    note

    If you do not know what your client URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your client.

    Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    If you want to make sure you've added your secrets correctly, run flyctl secrets list in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.

    Deploy to a Fly.io App

    While still in the .wasp/build/ directory, run:

    flyctl deploy --remote-only --config ../../fly.toml

    This will build and deploy the backend of your Wasp app on Fly.io to https://<app-name>.fly.dev 🤘🎸

    Now, if you haven't, you can deploy your client and add the client URL by running flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_client>. We suggest using Netlify for your client, but you can use any static hosting provider.

    Additionally, some useful flyctl commands:

    flyctl logs
    flyctl secrets list
    flyctl ssh console

    Redeploying After Wasp Builds

    When you rebuild your Wasp app (with wasp build), it will remove your .wasp/build/ directory. In there, you may have a fly.toml from any prior Fly.io deployments.

    While we will improve this process in the future, in the meantime, you have a few options:

    1. Copy the fly.toml file to a versioned directory, like your Wasp project dir.

      From there, you can reference it in flyctl deploy --config <path> commands, like above.

    2. Backup the fly.toml file somewhere before running wasp build, and copy it into .wasp/build/ after.

      When the fly.toml file exists in .wasp/build/ dir, you do not need to specify the --config <path>.

    3. Run flyctl config save -a <app-name> to regenerate the fly.toml file from the remote state stored in Fly.io.

    Netlify (client)

    We'll show how to deploy the client on Netlify.

    Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and Netlify CLI installed to follow these instructions.

    Make sure you are logged in with Netlify CLI. You can check if you are logged in with netlify status, and if you are not, you can log in with netlify login.

    First, make sure you have built the Wasp app. We'll build the client web app next.

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    We can now deploy the client with:

    netlify deploy

    Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.

    The final step is to run:

    netlify deploy --prod

    That is it! Your client should be live at https://<app-name>.netlify.app

    note

    Make sure you set this URL as the WASP_WEB_CLIENT_URL environment variable in your server hosting environment (e.g., Fly.io or Heroku).

    Railway (server, client and database)

    We will show how to deploy the client, the server, and provision a database on Railway.

    Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their plans page for more info.

    Prerequisites

    To get started, follow these steps:

    1. Make sure your Wasp app is built by running wasp build in the project dir.

    2. Create a Railway account

      Free Tier

      Sign up with your GitHub account to be eligible for the free tier

    3. Install the Railway CLI

    4. Run railway login and a browser tab will open to authenticate you.

    Create New Project

    Let's create our Railway project:

    1. Go to your Railway dashboard, click on New Project, and select Provision PostgreSQL from the dropdown menu.
    2. Once it initializes, right-click on the New button in the top right corner and select Empty Service.
    3. Once it initializes, click on it, go to Settings > General and change the name to server
    4. Go ahead and create another empty service and name it client

    Changing the name

    Deploy Your App to Railway

    Setup Domains

    We'll need the domains for both the server and client services:

    1. Go to the server instance's Settings tab, and click Generate Domain.
    2. Do the same under the client's Settings.

    Copy the domains as we will need them later.

    Deploying the Server

    Let's deploy our server first:

    1. Move into your app's .wasp/build/ directory:

      cd .wasp/build
    2. Link your app build to your newly created Railway project:

      railway link
    3. Go into the Railway dashboard and set up the required env variables:

      Open the Settings and go to the Variables tab:

      • click Variable reference and select DATABASE_URL (it will populate it with the correct value)

      • add WASP_WEB_CLIENT_URL - enter the client domain (e.g. https://client-production-XXXX.up.railway.app)

      • add WASP_SERVER_URL - enter the server domain (e.g. https://server-production-XXXX.up.railway.app)

      • add JWT_SECRET - enter a random string at least 32 characters long (use an online generator)

        Using an external auth method?

        If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    4. Push and deploy the project:

    railway up

    Select server when prompted with Select Service.

    Railway will now locate the Dockerfile and deploy your server 👍

    Deploying the Client

    1. Next, change into your app's frontend build directory .wasp/build/web-app:

      cd web-app
    2. Create the production build, using the server domain as the REACT_APP_API_URL:

      npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
    3. Next, we want to link this specific frontend directory to our project as well:

      railway link
    4. We need to configure Railway's static hosting for our client.

      Setting Up Static Hosting

      Copy the build folder within the web-app directory to dist:

      cp -r build dist

      We'll need to create the following files:

      • Dockerfile with:

        Dockerfile
        FROM pierrezemb/gostatic
        CMD [ "-fallback", "index.html" ]
        COPY ./dist/ /srv/http/
      • .dockerignore with:

        .dockerignore
        node_modules/

      You'll need to repeat these steps each time you run wasp build as it will remove the .wasp/build/web-app directory.

      Here's a useful shell script to do the process

      If you want to automate the process, save the following as deploy_client.sh in the root of your project:

      deploy_client.sh
      #!/usr/bin/env bash

      if [ -z "$REACT_APP_API_URL" ]
      then
      echo "REACT_APP_API_URL is not set"
      exit 1
      fi

      wasp build
      cd .wasp/build/web-app

      npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build

      cp -r build dist

      dockerfile_contents=$(cat <<EOF
      FROM pierrezemb/gostatic
      CMD [ "-fallback", "index.html" ]
      COPY ./dist/ /srv/http/
      EOF
      )

      dockerignore_contents=$(cat <<EOF
      node_modules/
      EOF
      )

      echo "$dockerfile_contents" > Dockerfile
      echo "$dockerignore_contents" > .dockerignore

      railway up

      Make it executable with:

      chmod +x deploy_client.sh

      You can run it with:

      REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
    5. Set the PORT environment variable to 8043 under the Variables tab.

    6. Once set, deploy the client and select client when prompted with Select Service:

    railway up

    Conclusion

    And now your Wasp should be deployed! 🐝 🚂 🚀

    Back in your Railway dashboard, click on your project and you should see your newly deployed services: PostgreSQL, Server, and Client.

    Updates & Redeploying

    When you make updates and need to redeploy:

    • run wasp build to rebuild your app
    • run railway up in the .wasp/build directory (server)
    • repeat all the steps in the .wasp/build/web-app directory (client)

    Heroku (server and database)

    We will show how to deploy the server and provision a database for it on Heroku.

    note

    Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

    As such, we recommend using an alternative provider like Fly.io for your first apps.

    You will need Heroku account, heroku CLI and docker CLI installed to follow these instructions.

    Make sure you are logged in with heroku CLI. You can check if you are logged in with heroku whoami, and if you are not, you can log in with heroku login.

    Set Up a Heroku App

    info

    You need to do this only once per Wasp app.

    Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:

    heroku create <app-name>

    Unless you have an external PostgreSQL database that you want to use, let's create a new database on Heroku and attach it to our app:

    heroku addons:create --app <app-name> heroku-postgresql:mini
    caution

    Heroku does not offer a free plan anymore and mini is their cheapest database instance - it costs $5/mo.

    Heroku will also set DATABASE_URL env var for us at this point. If you are using an external database, you will have to set it up yourself.

    The PORT env var will also be provided by Heroku, so the ones left to set are the JWT_SECRET, WASP_WEB_CLIENT_URL and WASP_SERVER_URL env vars:

    heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
    heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_client_will_be_deployed>
    heroku config:set --app <app-name> WASP_SERVER_URL=<url_of_where_server_will_be_deployed>
    note

    If you do not know what your client URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your client.

    Deploy to a Heroku App

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    assuming you were at the root of your Wasp project at that moment.

    Log in to Heroku Container Registry:

    heroku container:login

    Build the docker image and push it to Heroku:

    heroku container:push --app <app-name> web

    App is still not deployed at this point. This step might take some time, especially the very first time, since there are no cached docker layers.

    Note for Apple Silicon Users

    Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of heroku container:push, users instead should:

    docker buildx build --platform linux/amd64 -t <app-name> .
    docker tag <app-name> registry.heroku.com/<app-name>/web
    docker push registry.heroku.com/<app-name>/web

    You are now ready to proceed to the next step.

    Deploy the pushed image and restart the app:

    heroku container:release --app <app-name> web

    This is it, the backend is deployed at https://<app-name>-XXXX.herokuapp.com 🎉

    Find out the exact app URL with:

    heroku info --app <app-name>

    Additionally, you can check out the logs with:

    heroku logs --tail --app <app-name>
    Using pg-boss with Heroku

    If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.

    Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js

    Koyeb (server, client and database)

    Check out the tutorial made by the team at Koyeb for detailed instructions on how to deploy a whole Wasp app on Koyeb: Using Wasp to Build Full-Stack Web Applications on Koyeb.

    The tutorial was written for Wasp v0.13.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/deployment/overview.html b/docs/0.13.0/advanced/deployment/overview.html index c4aa44dcda..5accf74e31 100644 --- a/docs/0.13.0/advanced/deployment/overview.html +++ b/docs/0.13.0/advanced/deployment/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It also runs any pending migrations.

    You can add extra steps to this multi-stage Dockerfile by creating your own Dockerfile in the project's root directory. If Wasp finds a Dockerfile in the project's root, it appends its contents at the bottom of the default multi-stage Dockerfile.

    Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

    A few things to keep in mind:

    • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
    • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
    • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

    Read more in the official Docker docs on multi-stage builds.

    To see what your project's (potentially combined) Dockerfile will look like, run:

    wasp dockerfile

    Join our Discord if you have any questions, or if you need more customization than this hook provides.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/email.html b/docs/0.13.0/advanced/email.html index 3c313033b8..a5e7ad0af0 100644 --- a/docs/0.13.0/advanced/email.html +++ b/docs/0.13.0/advanced/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Sending Emails

    With Wasp's email-sending feature, you can easily integrate email functionality into your web application.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    Choose from one of the providers:

    • Dummy (development only),
    • Mailgun,
    • SendGrid
    • or the good old SMTP.

    Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

    Sending Emails

    Before jumping into details about setting up various providers, let's see how easy it is to send emails.

    You import the emailSender that is provided by the wasp/server/email module and call the send method on it.

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    Read more about the send method in the API Reference.

    The send method returns an object with the status of the sent email. It varies depending on the provider you use.

    Providers

    We'll go over all of the available providers in the next section. For some of them, you'll need to set up some env variables. You can do that in the .env.server file.

    Using the Dummy Provider

    Dummy Provider is not for production use

    The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    To speed up development, Wasp offers a Dummy email sender that console.logs the emails in the console. Since it doesn't send emails for real, it doesn't require any setup.

    Set the provider to Dummy in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Dummy,
    }
    }

    Using the SMTP Provider

    First, set the provider to SMTP in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SMTP,
    }
    }

    Then, add the following env variables to your .env.server file.

    .env.server
    SMTP_HOST=
    SMTP_USERNAME=
    SMTP_PASSWORD=
    SMTP_PORT=

    Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

    Using the Mailgun Provider

    Set the provider to Mailgun in the main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Mailgun,
    }
    }

    Then, get the Mailgun API key and domain and add them to your .env.server file.

    Getting the API Key and Domain

    1. Go to Mailgun and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    4. Go to Domains and create a new domain.
    5. Copy the domain and add it to your .env.server file.
    .env.server
    MAILGUN_API_KEY=
    MAILGUN_DOMAIN=

    Using the SendGrid Provider

    Set the provider field to SendGrid in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SendGrid,
    }
    }

    Then, get the SendGrid API key and add it to your .env.server file.

    Getting the API Key

    1. Go to SendGrid and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    .env.server
    SENDGRID_API_KEY=

    API Reference

    emailSender dict

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    The emailSender dict has the following fields:

    • provider: Provider required

      The provider you want to use. Choose from Dummy, SMTP, Mailgun or SendGrid.

      Dummy Provider is not for production use

      The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    • defaultFrom: dict

      The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

    JavaScript API

    Using the emailSender in :

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    The send method accepts an object with the following fields:

    • from: object

      The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

      • name: string

        The name of the sender.

      • email: string

        The email address of the sender.

    • to: string required

      The recipient's email address.

    • subject: string required

      The subject of the email.

    • text: string required

      The text version of the email.

    • html: string required

      The HTML version of the email

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/jobs.html b/docs/0.13.0/advanced/jobs.html index 3c47354fe4..007a4cb0ac 100644 --- a/docs/0.13.0/advanced/jobs.html +++ b/docs/0.13.0/advanced/jobs.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Recurring Jobs

    In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

    What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

    Wasp supports background jobs that can help you with this:

    • Jobs persist between server restarts,
    • Jobs can be retried if they fail,
    • Jobs can be delayed until a future time,
    • Jobs can have a recurring schedule.

    Using Jobs

    Job Definition and Usage

    Let's write an example Job that will print a message to the console and return a list of tasks from the database.

    1. Start by creating a Job declaration in your .wasp file:

      main.wasp
      job mySpecialJob {
      executor: PgBoss,
      perform: {
      fn: import { foo } from "@src/workers/bar"
      },
      entities: [Task],
      }
    2. After declaring the Job, implement its worker function:

      src/workers/bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
      The worker function

      The worker function must be an async function. The function's return value represents the Job's result.

      The worker function accepts two arguments:

      • args: The data passed into the job when it's submitted.
      • context: { entities }: The context object containing entities you put in the Job declaration.
    3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'

      const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

      // Or, if you'd prefer it to execute in the future, just add a .delay().
      // It takes a number of seconds, Date, or ISO date string.
      await mySpecialJob
      .delay(10)
      .submit({ name: "Johnny" })

    And that's it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

    In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

    Recurring Jobs

    If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar"
    },
    schedule: {
    cron: "0 * * * *",
    args: {=json { "job": "args" } json=} // optional
    }
    }

    In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

    API Reference

    Declaring Jobs

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar",
    executorOptions: {
    pgBoss: {=json { "retryLimit": 1 } json=}
    }
    },
    schedule: {
    cron: "*/5 * * * *",
    args: {=json { "foo": "bar" } json=},
    executorOptions: {
    pgBoss: {=json { "retryLimit": 0 } json=}
    }
    },
    entities: [Task],
    }

    The Job declaration has the following fields:

    • executor: JobExecutor required

      Job executors

      Our jobs need job executors to handle the scheduling, monitoring, and execution.

      PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires your app.db.system to be PostgreSQL.

      We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

      info

      Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

      pg-boss details

      pg-boss provides many useful features, which can be found here.

      When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

      If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

      pg-boss considerations

      • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
        • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
      • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
        • If you remove a schedule from a job, you will need to do the above as well.
      • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
      • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
    • perform: dict required

      • fn: ExtImport required

        • An async function that performs the work. Since Wasp executes Jobs on the server, the import path must lead to a NodeJS file.
        • It receives the following arguments:
          • args: Input: The data passed to the job when it's submitted.
          • context: { entities: Entities }: The context object containing any declared entities.

        Here's an example of a perform.fn function:

        src/workers/bar.js
        export const foo = async ({ name }, context) => {
        console.log(`Hello ${name}!`)
        const tasks = await context.entities.Task.findMany({})
        return { tasks }
        }
      • executorOptions: dict

        Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

        • pgBoss: JSON

          See the docs for pg-boss.

    • schedule: dict

      • cron: string required

        A 5-placeholder format cron expression string. See rationale for minute-level precision here.

        If you need help building cron expressions, Check out Crontab guru.

      • args: JSON

        The arguments to pass to the perform.fn function when invoked.

      • executorOptions: dict

        Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

        • pgBoss: JSON

          See the docs for pg-boss.

    • entities: [Entity]

      A list of entities you wish to use inside your Job (similar to Queries and Actions).

    JavaScript API

    • Importing a Job:

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'
    • submit(jobArgs, executorOptions)

      • jobArgs: Input

      • executorOptions: object

        Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

      someAction.js
      const submittedJob = await mySpecialJob.submit({ job: "args" })
    • delay(startAfter)

      • startAfter: int | string | Date required

        Delaying the invocation of the job handler. The delay can be one of:

        • Integer: number of seconds to delay. [Default 0]
        • String: ISO date string to run at.
        • Date: Date to run at.
      someAction.js
      const submittedJob = await mySpecialJob
      .delay(10)
      .submit({ job: "args" }, { "retryLimit": 2 })

    Tracking

    The return value of submit() is an instance of SubmittedJob, which has the following fields:

    • jobId: The ID for the job in that executor.
    • jobName: The name of the job you used in your .wasp file.
    • executorName: The Symbol of the name of the job executor.

    There are also some namespaced, job executor-specific objects.

    • For pg-boss, you may access: pgBoss
      • details(): pg-boss specific job detail information. Reference
      • cancel(): attempts to cancel a job. Reference
      • resume(): attempts to resume a canceled job. Reference
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/links.html b/docs/0.13.0/advanced/links.html index f45c798bff..c41c6028d8 100644 --- a/docs/0.13.0/advanced/links.html +++ b/docs/0.13.0/advanced/links.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Type-Safe Links

    If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

    After you defined a route:

    main.wasp
    route TaskRoute { path: "/task/:id", to: TaskPage }
    page TaskPage { ... }

    You can get the benefits of type-safe links by using the Link component from wasp/client/router:

    TaskList.tsx
    import { Link } from 'wasp/client/router'

    export const TaskList = () => {
    // ...

    return (
    <div>
    {tasks.map((task) => (
    <Link
    key={task.id}
    to="/task/:id"
    {/* 👆 You must provide a valid path here */}
    params={{ id: task.id }}>
    {/* 👆 All the params must be correctly passed in */}
    {task.description}
    </Link>
    ))}
    </div>
    )
    }

    Using Search Query & Hash

    You can also pass search and hash props to the Link component:

    TaskList.tsx
    <Link
    to="/task/:id"
    params={{ id: task.id }}
    search={{ sortBy: 'date' }}
    hash="comments"
    >
    {task.description}
    </Link>

    This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

    The routes Object

    You can also get all the pages in your app with the routes object:

    TaskList.tsx
    import { routes } from 'wasp/client/router'

    const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

    This will result in a link like this: /task/1.

    You can also pass search and hash props to the build function. Check out the API Reference for more details.

    API Reference

    The Link component accepts the following props:

    • to required

      • A valid Wasp Route path from your main.wasp file.
    • params: { [name: string]: string | number } required (if the path contains params)

      • An object with keys and values for each param in the path.
      • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
    • search: string[][] | Record<string, string> | string | URLSearchParams

      • Any valid input for URLSearchParams constructor.
      • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
    • hash: string

    • all other props that the react-router-dom's Link component accepts

    routes Object

    The routes object contains a function for each route in your app.

    router.tsx
    export const routes = {
    // RootRoute has a path like "/"
    RootRoute: {
    build: (options?: {
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }) => // ...
    },

    // DetailRoute has a path like "/task/:id/:something?"
    DetailRoute: {
    build: (
    options: {
    params: { id: ParamValue; something?: ParamValue; },
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }
    ) => // ...
    }
    }

    The params object is required if the route contains params. The search and hash parameters are optional.

    You can use the routes object like this:

    import { routes } from 'wasp/client/router'

    const linkToRoot = routes.RootRoute.build()
    const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/middleware-config.html b/docs/0.13.0/advanced/middleware-config.html index 7db8cfbd68..601ed6a400 100644 --- a/docs/0.13.0/advanced/middleware-config.html +++ b/docs/0.13.0/advanced/middleware-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Configuring Middleware

    Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

    Default Global Middleware 🌍

    Wasp's Express server has the following middleware by default:

    • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

    • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

      note

      CORS middleware is required for the frontend to communicate with the backend.

    • Morgan: HTTP request logger middleware.

    • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

      note

      JSON middleware is required for Operations to function properly.

    • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

    • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

    Customization

    You have three places where you can customize middleware:

    1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

      Modifying global middleware

      Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

    2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

    3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

      • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

    Default Middleware Definitions

    Below is the actual definitions of default middleware which you can override.

    const defaultGlobalMiddleware = new Map([
    ['helmet', helmet()],
    ['cors', cors({ origin: config.allowedCORSOrigins })],
    ['logger', logger('dev')],
    ['express.json', express.json()],
    ['express.urlencoded', express.urlencoded({ extended: false })],
    ['cookieParser', cookieParser()]
    ])

    1. Customize Global Middleware

    If you would like to modify the middleware for all operations and APIs, you can do something like:

    main.wasp
    app todoApp {
    // ...

    server: {
    setupFn: import setup from "@src/serverSetup",
    middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"
    },
    }
    src/serverSetup.js
    import cors from 'cors'
    import { config } from 'wasp/server'

    export const serverMiddlewareFn = (middlewareConfig) => {
    // Example of adding extra domains to CORS.
    middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
    return middlewareConfig
    }

    2. Customize api-specific Middleware

    If you would like to modify the middleware for a single API, you can do something like:

    main.wasp
    // ...

    api webhookCallback {
    fn: import { webhookCallback } from "@src/apis",
    middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",
    httpRoute: (POST, "/webhook/callback"),
    auth: false
    }
    src/apis.js
    import express from 'express'

    export const webhookCallback = (req, res, _context) => {
    res.json({ msg: req.body.length })
    }

    export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
    console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

    middlewareConfig.delete('express.json')
    middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

    return middlewareConfig
    }

    note

    This gets installed on a per-method basis. Behind the scenes, this results in code like:

    router.post('/webhook/callback', webhookCallbackMiddleware, ...)

    3. Customize Per-Path Middleware

    If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

    main.wasp
    // ...

    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo/bar"
    }
    src/apis.js
    export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
    const customMiddleware = (_req, _res, next) => {
    console.log('fooBarNamespaceMiddlewareFn: custom middleware')
    next()
    }

    middlewareConfig.set('custom.middleware', customMiddleware)

    return middlewareConfig
    }
    note

    This gets installed at the router level for the path. Behind the scenes, this results in something like:

    router.use('/foo/bar', fooBarNamespaceMiddleware)
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/advanced/web-sockets.html b/docs/0.13.0/advanced/web-sockets.html index 3bf4dc2599..d3bf9ffa7b 100644 --- a/docs/0.13.0/advanced/web-sockets.html +++ b/docs/0.13.0/advanced/web-sockets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Web Sockets

    Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

    We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

    To get started, you need to:

    1. Define your WebSocket logic on the server.
    2. Enable WebSockets in your Wasp file, and connect it with your server logic.
    3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
    4. Optionally, type the WebSocket events and payloads for full-stack type safety.

    Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

    Turn On WebSockets in Your Wasp File

    We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    Defining the Events Handler

    Let's define the WebSockets server with all of the events and handler functions.

    webSocketFn Function

    On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

    You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

    This is how we can define our webSocketFn function:

    src/webSocket.js
    import { v4 as uuidv4 } from 'uuid'
    import { getFirstProviderUserId } from 'wasp/auth'

    export const webSocketFn = (io, context) => {
    io.on('connection', (socket) => {
    const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
    console.log('a user connected: ', username)

    socket.on('chatMessage', async (msg) => {
    console.log('message: ', msg)
    io.emit('chatMessage', { id: uuidv4(), username, text: msg })
    // You can also use your entities here:
    // await context.entities.SomeEntity.create({ someField: msg })
    })
    })
    }

    Using the WebSocket On The Client

    useSocket Hook

    Client access to WebSockets is provided by the useSocket hook. It returns:

    • socket: Socket for sending and receiving events.
    • isConnected: boolean for showing a display of the Socket.IO connection status.
      • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
      • If you set autoConnect: false in your Wasp file, then you should call these as needed.

    All components using useSocket share the same underlying socket.

    useSocketListener Hook

    Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

    src/ChatPage.jsx
    import React, { useState } from 'react'
    import {
    useSocket,
    useSocketListener,
    } from 'wasp/client/webSocket'

    export const ChatPage = () => {
    const [messageText, setMessageText] = useState('')
    const [messages, setMessages] = useState([])
    const { socket, isConnected } = useSocket()

    useSocketListener('chatMessage', logMessage)

    function logMessage(msg) {
    setMessages((priorMessages) => [msg, ...priorMessages])
    }

    function handleSubmit(e) {
    e.preventDefault()
    socket.emit('chatMessage', messageText)
    setMessageText('')
    }

    const messageList = messages.map((msg) => (
    <li key={msg.id}>
    <em>{msg.username}</em>: {msg.text}
    </li>
    ))
    const connectionIcon = isConnected ? '🟢' : '🔴'

    return (
    <>
    <h2>Chat {connectionIcon}</h2>
    <div>
    <form onSubmit={handleSubmit}>
    <div>
    <div>
    <input
    type="text"
    value={messageText}
    onChange={(e) => setMessageText(e.target.value)}
    />
    </div>
    <div>
    <button type="submit">Submit</button>
    </div>
    </div>
    </form>
    <ul>{messageList}</ul>
    </div>
    </>
    )
    }

    API Reference

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    The webSocket dict has the following fields:

    • fn: WebSocketFn required

      The function that defines the WebSocket events and handlers.

    • autoConnect: bool

      Whether to automatically connect to the WebSocket server. Default: true.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/email.html b/docs/0.13.0/auth/email.html index 43876918eb..5b3a4d45bb 100644 --- a/docs/0.13.0/auth/email.html +++ b/docs/0.13.0/auth/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Email

    Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

    Auth UI

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Setting Up Email Authentication

    We'll need to take the following steps to set up email authentication:

    1. Enable email authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages
    5. Set up the email sender

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }

    // Defining User entity
    entity User { ... }

    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Email Authentication in main.wasp

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable email authentication
    email: {
    // 3. Specify the email from field
    fromField: {
    name: "My App Postman",
    email: "hello@itsme.com"
    },
    // 4. Specify the email verification and password reset options (we'll talk about them later)
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    },
    },
    },
    onAuthFailedRedirectTo: "/login",
    onAuthSucceededRedirectTo: "/"
    },
    }

    Read more about the email auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    main.wasp
    // 5. Define the user entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    psl=}

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...

    // 6. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { RequestPasswordReset } from "@src/pages/auth.jsx",
    }

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { PasswordReset } from "@src/pages/auth.jsx",
    }

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { EmailVerification } from "@src/pages/auth.jsx",
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import {
    LoginForm,
    SignupForm,
    VerifyEmailForm,
    ForgotPasswordForm,
    ResetPasswordForm,
    } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    <br />
    <span className="text-sm font-medium text-gray-900">
    Forgot your password? <Link to="/request-password-reset">reset it</Link>
    .
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    export function EmailVerification() {
    return (
    <Layout>
    <VerifyEmailForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    export function RequestPasswordReset() {
    return (
    <Layout>
    <ForgotPasswordForm />
    </Layout>
    );
    }

    export function PasswordReset() {
    return (
    <Layout>
    <ResetPasswordForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    5. Set up an Email Sender

    To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

    We'll use the Dummy provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the supported email providers.

    To set up the Dummy provider to send emails, add the following to the main.wasp file:

    main.wasp
    app myApp {
    // ...
    // 7. Set up the email sender
    emailSender: {
    provider: Dummy,
    }
    }

    Conclusion

    That's it! We have set up email authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the auth overview.

    Login and Signup Flows

    Login

    Auth UI

    Signup

    Auth UI

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

    3. Allowing registration for unverified emails

      If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

    4. Password validation

      Read more about the default password validation rules and how to override them in auth overview docs.

    Email Verification Flow

    Automatic email verification in development

    In development mode, you can skip the email verification step by setting the SKIP_EMAIL_VERIFICATION_IN_DEV environment variable to true in your .env.server file:

    .env.server
    SKIP_EMAIL_VERIFICATION_IN_DEV=true

    This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app.

    By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

    Our setup looks like this:

    main.wasp
    // ...

    emailVerification: {
    clientRoute: EmailVerificationRoute,
    }

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

    The content of the e-mail can be customized, read more about it here.

    Email Verification Page

    We defined our email verification page in the auth.tsx file.

    Auth UI

    Password Reset Flow

    Users can request a password and then they'll receive an e-mail with a link to reset their password.

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

    Our setup in main.wasp looks like this:

    main.wasp
    // ...

    passwordReset: {
    clientRoute: PasswordResetRoute,
    }

    Request Password Reset Page

    Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

    Request password reset page

    Password Reset Page

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

    Request password reset page

    Users can enter their new password there.

    The content of the e-mail can be customized, read more about it here.

    Creating a Custom Sign-up Action

    Creating a custom sign-up action

    We don't recommend creating a custom sign-up action unless you have a good reason to do so. It is a complex process and you can easily make a mistake that will compromise the security of your app.

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidEmail,
    createProviderId,
    sanitizeAndSerializeProviderData,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    createUser,
    createEmailVerificationLink,
    sendEmailVerificationEmail,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidEmail(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('email', args.email)
    const existingAuthIdentity = await findAuthIdentity(providerId)

    if (existingAuthIdentity) {
    const providerData = deserializeAndSanitizeProviderData(existingAuthIdentity.providerData)
    // Your custom code here
    } else {
    // sanitizeAndSerializeProviderData will hash the user's password
    const newUserProviderData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    isEmailVerified: false,
    emailVerificationSentAt: null,
    passwordResetSentAt: null,
    })
    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )

    // Verification link links to a client route e.g. /email-verification
    const verificationLink = await createEmailVerificationLink(args.email, '/email-verification');
    try {
    await sendEmailVerificationEmail(
    args.email,
    {
    from: {
    name: "My App Postman",
    email: "hello@itsme.com",
    },
    to: args.email,
    subject: "Verify your email",
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    }
    );
    } catch (e: unknown) {
    console.error("Failed to send email verification email:", e);
    throw new HttpError(500, "Failed to send email verification email.");
    }
    }
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Email

    • ensureValidEmail(args)

      Checks if the email is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    getEmail

    If you are looking to access the user's email in your code, you can do that by accessing the info about the user that is stored in the user.auth.identities array.

    To make things a bit easier for you, Wasp offers the getEmail helper.

    The getEmail helper returns the user's email or null if the user doesn't have an email auth identity.

    src/MainPage.jsx
    import { getEmail } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const email = getEmail(user)
    // ...
    }
    src/tasks.js
    import { getEmail } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const email = getEmail(context.user)
    // ...
    }

    API Reference

    Let's go over the options we can specify when using email authentication.

    userEntity fields

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    // We'll explain these options below
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the email dict

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    userSignupFields: import { userSignupFields } from "@src/auth.js",
    fromField: {
    name: "My App",
    email: "hello@itsme.com"
    },
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js",
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js",
    },
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })
    Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields).

    fromField: EmailFromField required

    fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

    It has the following fields:

    • name: name of the sender
    • email: e-mail address of the sender required

    emailVerification: EmailVerificationConfig required

    emailVerification is a dict that specifies the details of the e-mail verification process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

      Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

      src/pages/EmailVerificationPage.jsx
      import { verifyEmail } from 'wasp/client/auth'
      ...
      await verifyEmail({ token });
      note

      We used Auth UI above to avoid doing this work of sending the token to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn can be done by defining a file in the src directory.

      src/email.js
      export const getVerificationEmailContent = ({ verificationLink }) => ({
      subject: 'Verify your email',
      text: `Click the link below to verify your email: ${verificationLink}`,
      html: `
      <p>Click the link below to verify your email</p>
      <a href="${verificationLink}">Verify email</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.

    passwordReset: PasswordResetConfig required

    passwordReset is a dict that specifies the password reset process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to reset their password. required

      Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

      src/pages/ForgotPasswordPage.jsx
      import { requestPasswordReset } from 'wasp/client/auth'
      ...
      await requestPasswordReset({ email });
      src/pages/PasswordResetPage.jsx
      import { resetPassword } from 'wasp/client/auth'
      ...
      await resetPassword({ password, token })
      note

      We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn is done by defining a function that looks like this:

      src/email.js
      export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
      subject: 'Password reset',
      text: `Click the link below to reset your password: ${passwordResetLink}`,
      html: `
      <p>Click the link below to reset your password</p>
      <a href="${passwordResetLink}">Reset password</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/entities.html b/docs/0.13.0/auth/entities.html index 9c7c3eae52..032c34bb72 100644 --- a/docs/0.13.0/auth/entities.html +++ b/docs/0.13.0/auth/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Auth Entities

    Wasp supports multiple different authentication methods and for each method, we need to store different information about the user. For example, if you are using the Username & password authentication method, we need to store the user's username and password. On the other hand, if you are using the Email authentication method, you will need to store the user's email, password and for example, their email verification status.

    Entities Explained

    To store user information, Wasp creates a few entities behind the scenes. In this section, we will explain what entities are created and how they are connected.

    User Entity

    When you want to add authentication to your app, you need to specify the user entity e.g. User in your Wasp file. This entity is a "business logic user" which represents a user of your app.

    You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address. You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created.

    entity User {=psl
    id Int @id @default(autoincrement())
    // Any other fields you want to store about the user
    psl=}

    You own the user entity and you can modify it as you wish. You can add new fields to it, remove fields from it, or change the type of the fields. You can also add new relations to it or remove existing relations from it.

    Auth Entities in a Wasp App
    Auth Entities in a Wasp App

    On the other hand, the Auth, AuthIdentity and Session entities are created behind the scenes and are used to store the user's login credentials. You as the developer don't need to care about this entity most of the time. Wasp owns these entities.

    In the case you want to create a custom signup action, you will need to use the Auth and AuthIdentity entities directly.

    Example App Model

    Let's imagine we created a simple tasks management app:

    • The app has email and Google-based auth.
    • Users can create tasks and see the tasks that they have created.

    Let's look at how would that look in the database:

    Example of Auth Entities
    Example of Auth Entities

    If we take a look at an example user in the database, we can see:

    • The business logic user, User is connected to multiple Task entities.
      • In this example, "Example User" has two tasks.
    • The User is connected to exactly one Auth entity.
    • Each Auth entity can have multiple AuthIdentity entities.
      • In this example, the Auth entity has two AuthIdentity entities: one for the email-based auth and one for the Google-based auth.
    • Each Auth entity can have multiple Session entities.
      • In this example, the Auth entity has one Session entity.
    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Auth Entity internal

    Wasp's internal Auth entity is used to connect the business logic user, User with the user's login credentials.

    entity Auth {=psl
    id String @id @default(uuid())
    userId Int? @unique
    // Wasp injects this relation on the User entity as well
    user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
    identities AuthIdentity[]
    sessions Session[]
    psl=}

    The Auth fields:

    • id is a unique identifier of the Auth entity.
    • userId is a foreign key to the User entity.
      • It is used to connect the Auth entity with the business logic user.
    • user is a relation to the User entity.
      • This relation is injected on the User entity as well.
    • identities is a relation to the AuthIdentity entity.
    • sessions is a relation to the Session entity.

    AuthIdentity Entity internal

    The AuthIdentity entity is used to store the user's login credentials for various authentication methods.

    entity AuthIdentity {=psl
    providerName String
    providerUserId String
    providerData String @default("{}")
    authId String
    auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade)

    @@id([providerName, providerUserId])
    psl=}

    The AuthIdentity fields:

    • providerName is the name of the authentication provider.
      • For example, email or google.
    • providerUserId is the user's ID in the authentication provider.
      • For example, the user's email or Google ID.
    • providerData is a JSON string that contains additional data about the user from the authentication provider.
    • authId is a foreign key to the Auth entity.
      • It is used to connect the AuthIdentity entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Session Entity internal

    The Session entity is used to store the user's session information. It is used to keep the user logged in between page refreshes.

    entity Session {=psl
    id String @id @unique
    expiresAt DateTime
    userId String
    auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade)

    @@index([userId])
    psl=}

    The Session fields:

    • id is a unique identifier of the Session entity.
    • expiresAt is the date when the session expires.
    • userId is a foreign key to the Auth entity.
      • It is used to connect the Session entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Accessing the Auth Fields

    If you are looking to access the user's email or username in your code, you can do that by accessing the info about the user that is stored in the AuthIdentity entity.

    Everywhere where Wasp gives you the user object, it also includes the auth relation with the identities relation. This means that you can access the auth identity info by using the user.auth.identities array.

    To make things a bit easier for you, Wasp offers a few helper functions that you can use to access the auth identity info.

    getEmail

    The getEmail helper returns the user's email or null if the user doesn't have an email auth identity.

    src/MainPage.jsx
    import { getEmail } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const email = getEmail(user)
    // ...
    }
    src/tasks.js
    import { getEmail } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const email = getEmail(context.user)
    // ...
    }

    getUsername

    The getUsername helper returns the user's username or null if the user doesn't have a username auth identity.

    src/MainPage.jsx
    import { getUsername } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const username = getUsername(user)
    // ...
    }
    src/tasks.js
    import { getUsername } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const username = getUsername(context.user)
    // ...
    }

    getFirstProviderUserId

    The getFirstProviderUserId helper returns the first user ID (e.g. username or email) that it finds for the user or null if it doesn't find any.

    As mentioned before, the providerUserId field is how providers identify our users. For example, the user's username in the case of the username auth or the user's email in the case of the email auth. This can be useful if you support multiple authentication methods and you need any ID that identifies the user in your app.

    src/MainPage.jsx
    import { getFirstProviderUserId } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const userId = getFirstProviderUserId(user)
    // ...
    }
    src/tasks.js
    import { getFirstProviderUserId } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const userId = getFirstProviderUserId(context.user)
    // ...
    }

    findUserIdentity

    You can find a specific auth identity by using the findUserIdentity helper function. This function takes a user and a providerName and returns the first providerName identity that it finds or null if it doesn't find any.

    Possible provider names are:

    • email
    • username
    • google
    • github

    This can be useful if you want to check if the user has a specific auth identity. For example, you might want to check if the user has an email auth identity or Google auth identity.

    src/MainPage.jsx
    import { findUserIdentity } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const emailIdentity = findUserIdentity(user, 'email')
    const googleIdentity = findUserIdentity(user, 'google')
    if (emailIdentity) {
    // ...
    } else if (googleIdentity) {
    // ...
    }
    // ...
    }
    src/tasks.js
    import { findUserIdentity } from 'wasp/client/auth'

    export const createTask = async (args, context) => {
    const emailIdentity = findUserIdentity(context.user, 'email')
    const googleIdentity = findUserIdentity(context.user, 'google')
    if (emailIdentity) {
    // ...
    } else if (googleIdentity) {
    // ...
    }
    // ...
    }

    Custom Signup Action

    Let's take a look at how you can use the Auth and AuthIdentity entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service.

    Custom Signup Examples

    In the Email section of the docs we give you an example for custom email signup and in the Username & password section of the docs we give you an example for custom username & password signup.

    Below is a simplified version of a custom signup action which you probably wouldn't use in your app but it shows you how you can use the Auth and AuthIdentity entities to create a custom signup action.

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    entities: [User]
    }
    src/auth/signup.js
    import {
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, { entities: { User } }) => {
    try {
    // Provider ID is a combination of the provider name and the provider user ID
    // And it is used to uniquely identify the user in your app
    const providerId = createProviderId('username', args.username)
    // sanitizeAndSerializeProviderData hashes the password and returns a JSON string
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )

    // This is equivalent to:
    // await User.create({
    // data: {
    // auth: {
    // create: {
    // identities: {
    // create: {
    // providerName: 'username',
    // providerUserId: args.username
    // providerData,
    // },
    // },
    // }
    // },
    // }
    // })
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    You can use whichever method suits your needs better: either the createUser function or Prisma's User.create method. The createUser function is a bit more convenient to use because it hides some of the complexity. On the other hand, the User.create method gives you more control over the data that is stored in the Auth and AuthIdentity entities.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/overview.html b/docs/0.13.0/auth/overview.html index c35e1b9276..7505cf0ae5 100644 --- a/docs/0.13.0/auth/overview.html +++ b/docs/0.13.0/auth/overview.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Overview

    Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box.

    Here's a 1-minute tour of how full-stack auth works in Wasp:

    Enabling auth for your app is optional and can be done by configuring the auth field of your app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute"
    }
    }

    //...

    Read more about the auth field options in the API Reference section.

    We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.

    Available auth methods

    Wasp supports the following auth methods:

    Click on each auth method for more details.

    Let's say we enabled the Username & password authentication.

    We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.

    We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.

    We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.

    Protecting a page with authRequired

    When declaring a page, you can set the authRequired property.

    If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.

    main.wasp
    page MainPage {
    component: import Main from "@src/pages/Main",
    authRequired: true
    }
    Requires auth method

    You can only use authRequired if your app uses one of the available auth methods.

    If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.

    Logout action

    We provide an action for logging out the user. Here's how you can use it:

    src/components/LogoutButton.jsx
    import { logout } from 'wasp/client/auth'

    const LogoutButton = () => {
    return <button onClick={logout}>Logout</button>
    }

    Accessing the logged-in user

    You can get access to the user object both on the server and on the client. The user object contains the logged-in user's data.

    The user object has all the fields that you defined in your User entity, plus the auth field which contains the auth identities connected to the user. For example, if the user signed up with their email, the user object might look something like this:

    const user = {
    id: "19c7d164-b5cb-4dde-a0cc-0daea77cf854",

    // Your entity's fields.
    address: "My address",
    // ...

    // Auth identities connected to the user.
    auth: {
    id: "26ab6f96-ed76-4ee5-9ac3-2fd0bf19711f",
    identities: [
    {
    providerName: "email",
    providerUserId: "some@email.com",
    providerData: { ... },
    },
    ]
    },
    }

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    On the client

    There are two ways to access the user object on the client:

    • the user prop
    • the useAuth hook

    Using the user prop

    If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:

    main.wasp
    // ...

    page AccountPage {
    component: import Account from "@src/pages/Account",
    authRequired: true
    }
    src/pages/Account.jsx
    import Button from './Button'
    import { logout } from 'wasp/client/auth'

    const AccountPage = ({ user }) => {
    return (
    <div>
    <Button onClick={logout}>Logout</Button>
    {JSON.stringify(user, null, 2)}
    </div>
    )
    }

    export default AccountPage

    Using the useAuth hook

    Wasp provides a React hook you can use in the client components - useAuth.

    This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.

    src/pages/MainPage.jsx
    import { useAuth, logout } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'
    import Todo from '../Todo'

    export function Main() {
    const { data: user } = useAuth()

    if (!user) {
    return (
    <span>
    Please <Link to="/login">login</Link> or{' '}
    <Link to="/signup">sign up</Link>.
    </span>
    )
    } else {
    return (
    <>
    <button onClick={logout}>Logout</button>
    <Todo />
    </>
    )
    }
    }
    tip

    Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.

    On the server

    Using the context.user object

    When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields and the auth identities connected to the user. We strip out the hashedPassword field from the identities for security reasons.

    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (task, context) => {
    if (!context.user) {
    throw new HttpError(403)
    }

    const Task = context.entities.Task
    return Task.create({
    data: {
    description: task.description,
    user: {
    connect: { id: context.user.id },
    },
    },
    })
    }

    To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.

    When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.

    Sessions

    Wasp's auth uses sessions to keep track of the logged-in user. The session is stored in localStorage on the client and in the database on the server. Under the hood, Wasp uses the excellent Lucia Auth v3 library for session management.

    When users log in, Wasp creates a session for them and stores it in the database. The session is then sent to the client and stored in localStorage. When users log out, Wasp deletes the session from the database and from localStorage.

    User Entity

    Password Hashing

    If you are saving a user's password in the database, you should never save it as plain text. You can use Wasp's helper functions for serializing and deserializing provider data which will automatically hash the password for you:

    main.wasp
    // ...

    action updatePassword {
    fn: import { updatePassword } from "@src/auth",
    }
    src/auth.js
    import {
    createProviderId,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    deserializeAndSanitizeProviderData,
    } from 'wasp/server/auth';

    export const updatePassword = async (args, context) => {
    const providerId = createProviderId('email', args.email)
    const authIdentity = await findAuthIdentity(providerId)
    if (!authIdentity) {
    throw new HttpError(400, "Unknown user")
    }

    const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData)

    // Updates the password and hashes it automatically.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: args.password,
    })
    }

    Default Validations

    When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in Auth UI or if you use the provided auth actions.

    If you decide to create your custom auth actions, you'll need to run the validations yourself.

    Default validations depend on the auth method you use.

    Username & Password

    If you use Username & password authentication, the default validations are:

    • The username must not be empty
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that usernames are stored in a case-insensitive manner.

    Email

    If you use Email authentication, the default validations are:

    • The email must not be empty and a valid email address
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that emails are stored in a case-insensitive manner.

    Customizing the Signup Process

    Sometimes you want to include extra fields in your signup process, like first name and last name and save them in the User entity.

    For this to happen:

    • you need to define the fields that you want saved in the database,
    • you need to customize the SignupForm (in the case of Email or Username & Password auth)

    Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.

    Let's see how to do both.

    1. Defining Extra Fields

    If we want to save some extra fields in our signup process, we need to tell our app they exist.

    We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    * We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

    First, we add the auth.methods.{authMethod}.userSignupFields field in our main.wasp file. The {authMethod} depends on the auth method you are using.

    For example, if you are using Username & Password, you would add the auth.methods.usernameAndPassword.userSignupFields field:

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    address String?
    psl=}

    Then we'll define the userSignupFields object in the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    Read more about the userSignupFields object in the API Reference.

    Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity.

    The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.

    Using Validation Libraries

    You can use any validation library you want to validate the fields. For example, you can use zod like this:

    Click to see the code
    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'
    import * as z from 'zod'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    const AddressSchema = z
    .string({
    required_error: 'Address is required',
    invalid_type_error: 'Address must be a string',
    })
    .min(10, 'Address must be at least 10 characters long')
    const result = AddressSchema.safeParse(data.address)
    if (result.success === false) {
    throw new Error(result.error.issues[0].message)
    }
    return result.data
    },
    })

    Now that we defined the fields, Wasp knows how to:

    1. Validate the data sent from the client
    2. Save the data to the database

    Next, let's see how to customize Auth UI to include those fields.

    2. Customizing the Signup Component

    Using Custom Signup Component

    If you are not using Wasp's Auth UI, you can skip this section. Just make sure to include the extra fields in your custom signup form.

    Read more about using the signup actions for:

    • email auth here
    • username & password auth here

    If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.

    Using a List of Extra Fields

    When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.

    Inside the list, there can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
    2. Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the react-hook-form object and the form state object as arguments.
    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    /* The address field is defined using an object */
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    /* The phone number is defined using a render function */
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    Read more about the extra fields in the API Reference.

    Using a Single Render Function

    Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.

    src/SignupPage.jsx
    import { SignupForm, FormItemGroup } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={(form, state) => {
    const username = form.watch('username')
    return (
    username && (
    <FormItemGroup>
    Hello there <strong>{username}</strong> 👋
    </FormItemGroup>
    )
    )
    }}
    />
    )
    }

    Read more about the render function in the API Reference.

    API Reference

    Auth Fields

    main.wasp
      title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute",
    }
    }

    //...

    app.auth is a dictionary with the following fields:

    userEntity: entity required

    The entity representing the user connected to your business logic.

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    methods: dict required

    A dictionary of auth methods enabled for the app.

    Click on each auth method for more details.

    onAuthFailedRedirectTo: String required

    The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true). Check out these essentials docs on auth to see an example of usage.

    onAuthSucceededRedirectTo: String

    The route to which Wasp will send a successfully authenticated after a successful login/signup. The default value is "/".

    note

    Automatic redirect on successful login only works when using the Wasp-provided Auth UI.

    Signup Fields Customization

    If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.methods.{authMethod}.userSignupFields field in your main.wasp file.

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    Then we'll export the userSignupFields object from the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    The userSignupFields object is an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    If the value that the function received is invalid, the function should throw an error.

    * We exclude the password field from this object to prevent it from being saved as plain text in the database. The password field is handled by Wasp's auth backend.

    SignupForm Customization

    To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.

    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    The extra fields can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.

      The objects have the following properties:

      • name required

        • the name of the field
      • label required

        • the label of the field (used in the UI)
      • type required

        • the type of the field, which can be input or textarea
      • validations

        • an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
    2. Render functions receive the react-hook-form object and the form state as arguments, and they can use them to render arbitrary UI elements.

      The render function has the following signature:

      (form: UseFormReturn, state: FormState) => React.ReactNode
      • form required

        • the react-hook-form object, read more about it in the react-hook-form docs
        • you need to use the form.register function to register your fields
      • state required

        • the form state object which has the following properties:
          • isLoading: boolean
            • whether the form is currently submitting
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/social-auth/github.html b/docs/0.13.0/auth/social-auth/github.html index e187643ba5..8c0ddba740 100644 --- a/docs/0.13.0/auth/social-auth/github.html +++ b/docs/0.13.0/auth/social-auth/github.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.

    Letting your users log in using their GitHub accounts turns the signup process into a breeze.

    Let's walk through enabling Github Authentication, explain some of the default settings, and show how to override them.

    Setting up Github Auth

    Enabling GitHub Authentication comes down to a series of steps:

    1. Enabling GitHub authentication in the Wasp file.
    2. Adding the User entity.
    3. Creating a GitHub OAuth app.
    4. Adding the necessary Routes and Pages
    5. Using Auth UI components in our Pages.

    Here's a skeleton of how our main.wasp should look like after we're done:

    main.wasp
    // Configuring the social authentication
    app myApp {
    auth: { ... }
    }

    // Defining entities
    entity User { ... }

    // Defining routes and pages
    route LoginRoute { ... }
    page LoginPage { ... }

    1. Adding Github Auth to Your Wasp File

    Let's start by properly configuring the Auth object:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the User entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable Github Auth
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    2. Add the User Entity

    Let's now define the app.auth.userEntity entity:

    main.wasp
    // ...
    // 3. Define the User entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // ...
    psl=}

    3. Creating a GitHub OAuth App

    To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it:

    1. Log into your GitHub account and navigate to: https://github.com/settings/developers.
    2. Select New OAuth App.
    3. Supply required information.
    GitHub Applications Screenshot
    • For Authorization callback URL:
      • For development, put: http://localhost:3001/auth/github/callback.
      • Once you know on which URL your API server will be deployed, you can create a new app with that URL instead e.g. https://your-server-url.com/auth/github/callback.
    1. Hit Register application.
    2. Hit Generate a new client secret on the next page.
    3. Copy your Client ID and Client secret as you'll need them in the next step.

    4. Adding Environment Variables

    Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

    .env.server
    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret

    5. Adding the Necessary Routes and Pages

    Let's define the necessary authentication Routes and Pages.

    Add the following code to your main.wasp file:

    main.wasp
    // ...

    // 6. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    6. Creating the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    Yay, we've successfully set up Github Auth! 🎉

    Github Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add gitHub: {} to the auth.methods dictionary to use it with default settings.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From GitHub

    We are using GitHub's API and its /user and /user/emails endpoints to get the user data.

    We combine the data from the two endpoints

    You'll find the emails in the emails property in the object that you receive in userSignupFields.

    This is because we combine the data from the /user and /user/emails endpoints if the user or user:email scope is requested.

    The data we receive from GitHub on the /user endpoint looks something this:

    {
    "login": "octocat",
    "id": 1,
    "name": "monalisa octocat",
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    // ...
    }

    And the data from the /user/emails endpoint looks something like this:

    [
    {
    "email": "octocat@github.com",
    "verified": true,
    "primary": true,
    "visibility": "public"
    }
    ]

    The fields you receive will depend on the scopes you requested. By default we don't specify any scopes. If you want to get the emails, you need to specify the user or user:email scope in the configFn function.

    For an up to date info about the data received from GitHub, please refer to the GitHub API documentation.

    Using the Data Received From GitHub

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    psl=}

    // ...
    src/auth/github.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    };

    export function getConfig() {
    return {
    scopes: ['user'],
    };
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The gitHub dict has the following properties:

    • configFn: ExtImport

      This function should return an object with the scopes for the OAuth provider.

      src/auth/github.js
      export function getConfig() {
      return {
      scopes: [],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/social-auth/google.html b/docs/0.13.0/auth/social-auth/google.html index 70b589bed3..6e46f6be93 100644 --- a/docs/0.13.0/auth/social-auth/google.html +++ b/docs/0.13.0/auth/social-auth/google.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It should have the following code:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }
    Auth UI

    Our pages use an automatically-generated Auth UI component. Read more about Auth UI components here.

    Conclusion

    Yay, we've successfully set up Google Auth! 🎉

    Google Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add google: {} to the auth.methods dictionary to use it with default settings:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From Google

    We are using Google's API and its /userinfo endpoint to fetch the user's data.

    The data received from Google is an object which can contain the following fields:

    [
    "name",
    "given_name",
    "family_name",
    "email",
    "email_verified",
    "aud",
    "exp",
    "iat",
    "iss",
    "locale",
    "picture",
    "sub"
    ]

    The fields you receive depend on the scopes you request. The default scope is set to profile only. If you want to get the user's email, you need to specify the email scope in the configFn function.

    For an up to date info about the data received from Google, please refer to the Google API documentation.

    Using the Data Received From Google

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    psl=}

    // ...
    src/auth/google.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    }

    export function getConfig() {
    return {
    scopes: ['profile', 'email'],
    }
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The google dict has the following properties:

    • configFn: ExtImport

      This function must return an object with the scopes for the OAuth provider.

      src/auth/google.js
      export function getConfig() {
      return {
      scopes: ['profile', 'email'],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/social-auth/keycloak.html b/docs/0.13.0/auth/social-auth/keycloak.html index 03de5f0423..eb7f69fce2 100644 --- a/docs/0.13.0/auth/social-auth/keycloak.html +++ b/docs/0.13.0/auth/social-auth/keycloak.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ It should have the following code:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }
    Auth UI

    Our pages use an automatically generated Auth UI component. Read more about Auth UI components here.

    Conclusion

    Yay, we've successfully set up Keycloak Auth!

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add keycloak: {} to the auth.methods dictionary to use it with default settings:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From Keycloak

    We are using Keycloak's API and its /userinfo endpoint to fetch the user's data.

    Keycloak user data
    {
    sub: '5adba8fc-3ea6-445a-a379-13f0bb0b6969',
    email_verified: true,
    name: 'Test User',
    preferred_username: 'test',
    given_name: 'Test',
    family_name: 'User',
    email: 'test@example.com'
    }

    The fields you receive will depend on the scopes you requested. The default scope is set to profile only. If you want to get the user's email, you need to specify the email scope in the configFn function.

    For up-to-date info about the data received from Keycloak, please refer to the Keycloak API documentation.

    Using the Data Received From Keycloak

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {
    configFn: import { getConfig } from "@src/auth/keycloak.js",
    userSignupFields: import { userSignupFields } from "@src/auth/keycloak.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    psl=}

    // ...
    src/auth/keycloak.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    }

    export function getConfig() {
    return {
    scopes: ['profile', 'email'],
    }
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {
    configFn: import { getConfig } from "@src/auth/keycloak.js",
    userSignupFields: import { userSignupFields } from "@src/auth/keycloak.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The keycloak dict has the following properties:

    • configFn: ExtImport

      This function must return an object with the scopes for the OAuth provider.

      src/auth/keycloak.js
      export function getConfig() {
      return {
      scopes: ['profile', 'email'],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/social-auth/overview.html b/docs/0.13.0/auth/social-auth/overview.html index a3b204416a..bac3303f17 100644 --- a/docs/0.13.0/auth/social-auth/overview.html +++ b/docs/0.13.0/auth/social-auth/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -31,7 +31,7 @@ Depending on the flag's value, you can redirect users to the appropriate signup step.

    For example:

    1. When the user lands on the homepage, check the value of user.isSignupComplete.
    2. If it's false, it means the user has started the signup process but hasn't yet chosen their username. Therefore, you can redirect them to EditUserDetailsPage where they can edit the username property.
    src/HomePage.jsx
    import { useAuth } from 'wasp/client/auth'
    import { Redirect } from 'react-router-dom'

    export function HomePage() {
    const { data: user } = useAuth()

    if (user.isSignupComplete === false) {
    return <Redirect to="/edit-user-details" />
    }

    // ...
    }

    Using the User's Provider Account Details

    Account details are provider-specific. Each provider has their own rules for defining the userSignupFields and configFn fields:

    UI Helpers

    Use Auth UI

    Auth UI is a common name for all high-level auth forms that come with Wasp.

    These include fully functional auto-generated login and signup forms with working social login buttons. If you're looking for the fastest way to get your auth up and running, that's where you should look.

    The UI helpers described below are lower-level and are useful for creating your custom forms.

    Wasp provides sign-in buttons and URLs for each of the supported social login providers.

    src/LoginPage.jsx
    import {
    GoogleSignInButton,
    googleSignInUrl,
    GitHubSignInButton,
    gitHubSignInUrl,
    } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <>
    <GoogleSignInButton />
    <GitHubSignInButton />
    {/* or */}
    <a href={googleSignInUrl}>Sign in with Google</a>
    <a href={gitHubSignInUrl}>Sign in with GitHub</a>
    </>
    )
    }

    If you need even more customization, you can create your custom components using signInUrls.

    API Reference

    Fields in the app.auth Dictionary and Overrides

    For more information on:

    • Allowed fields in app.auth
    • userSignupFields and configFn functions

    Check the provider-specific API References:

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/ui.html b/docs/0.13.0/auth/ui.html index a3e7978e1d..36323ab437 100644 --- a/docs/0.13.0/auth/ui.html +++ b/docs/0.13.0/auth/ui.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Auth UI

    To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

    Below we cover all of the available UI components and how to use them.

    Auth UI

    Overview

    After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

    Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    },
    // ...
    }
    }

    You'll get the following UI:

    Auth UI

    And then if you enable Google and Github:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    google: {},
    github: {},
    },
    // ...
    }
    }

    The form will automatically update to look like this:

    Auth UI

    Let's go through all of the available components and how to use them.

    Auth Components

    The following components are available for you to use in your app:

    Login Form

    Used with Username & Password, Email, Github, Google and Keycloak authentication.

    Login form

    You can use the LoginForm component to build your login page:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage.jsx"
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    // Use it like this
    export function LoginPage() {
    return <LoginForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Signup Form

    Used with Username & Password, Email, Github, Google and Keycloak authentication.

    Signup form

    You can use the SignupForm component to build your signup page:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage.jsx"
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    // Use it like this
    export function SignupPage() {
    return <SignupForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Read more about customizing the signup process like adding additional fields or extra UI in the Auth Overview section.

    Forgot Password Form

    Used with Email authentication.

    If users forget their password, they can use this form to reset it.

    Forgot password form

    You can use the ForgotPasswordForm component to build your own forgot password page:

    main.wasp
    // ...

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage.jsx"
    }
    src/ForgotPasswordPage.jsx
    import { ForgotPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ForgotPasswordPage() {
    return <ForgotPasswordForm />
    }

    Reset Password Form

    Used with Email authentication.

    After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

    Reset password form

    You can use the ResetPasswordForm component to build your reset password page:

    main.wasp
    // ...

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { ResetPasswordPage } from "@src/ResetPasswordPage.jsx"
    }
    src/ResetPasswordPage.jsx
    import { ResetPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ResetPasswordPage() {
    return <ResetPasswordForm />
    }

    Verify Email Form

    Used with Email authentication.

    After users sign up, they will receive an email with a link to this form where they can verify their email.

    Verify email form

    You can use the VerifyEmailForm component to build your email verification page:

    main.wasp
    // ...

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { VerifyEmailPage } from "@src/VerifyEmailPage.jsx"
    }
    src/VerifyEmailPage.jsx
    import { VerifyEmailForm } from 'wasp/client/auth'

    // Use it like this
    export function VerifyEmailPage() {
    return <VerifyEmailForm />
    }

    Customization 💅🏻

    You customize all of the available forms by passing props to them.

    Props you can pass to all of the forms:

    1. appearance - customize the form colors (via design tokens)
    2. logo - path to your logo
    3. socialLayout - layout of the social buttons, which can be vertical or horizontal

    1. Customizing the Colors

    We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

    List of all available tokens

    See the list of all available tokens which you can override.

    src/appearance.js
    export const authAppearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { authAppearance } from './appearance'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass the appearance object to the form
    appearance={authAppearance}
    />
    )
    }

    We recommend defining your appearance in a separate file and importing it into your components.

    You can add your logo to the Auth UI by passing the logo prop to any of the components.

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import Logo from './logo.png'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the path to your logo
    logo={Logo}
    />
    )
    }

    3. Social Buttons Layout

    You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

    If we pass in vertical:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the socialLayout prop
    socialLayout="vertical"
    />
    )
    }

    We get this:

    Vertical social buttons

    Let's Put Everything Together 🪄

    If we provide the logo and custom colors:

    src/appearance.js
    export const appearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    import { authAppearance } from './appearance'
    import todoLogo from './todoLogo.png'

    export function LoginPage() {
    return <LoginForm appearance={appearance} logo={todoLogo} />
    }

    We get a form looking like this:

    Custom login form
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/auth/username-and-pass.html b/docs/0.13.0/auth/username-and-pass.html index beefd0719b..f843df820c 100644 --- a/docs/0.13.0/auth/username-and-pass.html +++ b/docs/0.13.0/auth/username-and-pass.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Username & Password

    Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.

    Setting Up Username & Password Authentication

    To set up username authentication we need to:

    1. Enable username authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }
    // Defining User entity
    entity User { ... }
    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Username Authentication

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable username authentication
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }

    Read more about the usernameAndPassword auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    main.wasp
    // 3. Define the user entity
    entity User {=psl
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    psl=}

    You can read more about how the User entity is connected to the rest of the auth system in the Auth Entities section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...
    // 4. Define the routes
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }
    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm, SignupForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    That's it! We have set up username authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the auth overview docs.

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Customizing the Auth Flow

    The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

    Read more about the default username and password validation rules in the auth overview docs.

    If you require more control in your authentication flow, you can achieve that in the following ways:

    1. Create your UI and use signup and login actions.
    2. Create your custom sign-up action which uses the lower-level API, along with your custom code.

    1. Using the signup and login actions

    login()

    An action for logging in the user.

    It takes two arguments:

    • username: string required

      Username of the user logging in.

    • password: string required

      Password of the user logging in.

    You can use it like this:

    src/pages/auth.jsx
    import { login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory, Link } from 'react-router-dom'

    export function LoginPage() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await login(username, password)
    history.push('/')
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }
    note

    When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

    signup()

    An action for signing up the user. This action does not log in the user, you still need to call login().

    It takes one argument:

    • userFields: object required

      It has the following fields:

      • username: string required

      • password: string required

      info

      By default, Wasp will only save the username and password fields. If you want to add extra fields to your signup process, read about defining extra signup fields.

    You can use it like this:

    src/pages/auth.jsx
    import { signup, login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory } from 'react-router-dom'
    import { Link } from 'react-router-dom'

    export function Signup() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await signup({
    username,
    password,
    })
    await login(username, password)
    history.push("/")
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }

    2. Creating your custom sign-up action

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidUsername,
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidUsername(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('username', args.username)
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Username

    • ensureValidUsername(args)

      Checks if the username is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    getUsername

    If you are looking to access the user's username in your code, you can do that by accessing the info about the user that is stored in the user.auth.identities array.

    To make things a bit easier for you, Wasp offers the getUsername helper.

    The getUsername helper returns the user's username or null if the user doesn't have a username auth identity.

    src/MainPage.jsx
    import { getUsername } from 'wasp/auth'

    const MainPage = ({ user }) => {
    const username = getUsername(user)
    // ...
    }
    src/tasks.js
    import { getUsername } from 'wasp/auth'

    export const createTask = async (args, context) => {
    const username = getUsername(context.user)
    // ...
    }

    API Reference

    userEntity fields

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the usernameAndPassword dict

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/email.js",
    },
    },
    onAuthFailedRedirectTo: "/login"
    }
    }
    // ...

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })
    Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields).
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/contact.html b/docs/0.13.0/contact.html index e436e6e63c..ea7bd2d03e 100644 --- a/docs/0.13.0/contact.html +++ b/docs/0.13.0/contact.html @@ -19,13 +19,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/0.13.0/contributing.html b/docs/0.13.0/contributing.html index 4badc27ef4..a3a295badd 100644 --- a/docs/0.13.0/contributing.html +++ b/docs/0.13.0/contributing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Contributing

    Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

    Some side notes to make your journey easier:

    1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

    2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

    3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

    Happy hacking!

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/backends.html b/docs/0.13.0/data-model/backends.html index b10a7a9428..0d19ce09f2 100644 --- a/docs/0.13.0/data-model/backends.html +++ b/docs/0.13.0/data-model/backends.html @@ -19,8 +19,8 @@ - - + +
    @@ -36,7 +36,7 @@ Whenever you modify the db.system field, make sure to run wasp db migrate-dev to apply the changes.

  • seeds: [ExtImport]

    Defines the seed functions you can use with the wasp db seed command to seed your database with initial data. Read the Seeding section for more details.

  • prisma: PrismaOptions

    Additional configuration for Prisma.

    main.wasp
    app MyApp {
    // ...
    db: {
    // ...
    prisma: {
    clientPreviewFeatures: ["postgresqlExtensions"],
    dbExtensions: [
    { name: "hstore", schema: "myHstoreSchema" },
    { name: "pg_trgm" },
    { name: "postgis", version: "2.1" },
    ]
    }
    }
    }

    It's a dictionary with the following fields:

    • clientPreviewFeatures : [string]

      Allows you to define Prisma client preview features, like for example, "postgresqlExtensions".

    • dbExtensions: DbExtension[]

      It allows you to define PostgreSQL extensions that should be enabled for your database. Read more about PostgreSQL extensions in Prisma.

      For each extension you define a dict with the following fields:

      • name: string required

        The name of the extension you would normally put in the Prisma file.

        schema.prisma
        extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
        // 👆 Extension name
      • map: string

        It sets the map argument of the extension. Explanation for the field from the Prisma docs:

        This is the database name of the extension. If this argument is not specified, the name of the extension in the Prisma schema must match the database name.

      • schema: string

        It sets the schema argument of the extension. Explanation for the field from the Prisma docs:

        This is the name of the schema in which to activate the extension's objects. If this argument is not specified, the current default object creation schema is used.

      • version: string

        It sets the version argument of the extension. Explanation for the field from the Prisma docs:

        This is the version of the extension to activate. If this argument is not specified, the value given in the extension's control file is used.

  • CLI Commands for Seeding the Database

    Use one of the following commands to run the seed functions:

    • wasp db seed

      If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.

    • wasp db seed <seed-name>

      This command runs the seed function with the specified name. The name is the identifier used in its import expression in the app.db.seeds list. For example, to run the seed function devSeedSimple which was defined like this:

      main.wasp
      app MyApp {
      // ...
      db: {
      // ...
      seeds: [
      // ...
      import { devSeedSimple } from "@src/dbSeeds.js",
      ]
      }
      }

      Use the following command:

      wasp db seed devSeedSimple
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/crud.html b/docs/0.13.0/data-model/crud.html index ab54236aed..d004ebd703 100644 --- a/docs/0.13.0/data-model/crud.html +++ b/docs/0.13.0/data-model/crud.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Automatic CRUD

    If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.

    Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.

    With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting Entities. As you update definitions for your Entities, Wasp automatically regenerates the backend logic.

    Early preview

    This feature is currently in early preview and we are actively working on it. Read more about our plans for CRUD operations.

    Overview

    Imagine we have a Task entity and we want to enable CRUD operations for it.

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    psl=}

    We can then define a new crud called Tasks.

    We specify to use the Task entity and we enable the getAll, get, create and update operations (let's say we don't need the delete operation).

    main.wasp
    crud Tasks {
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // by default only logged in users can perform operations
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js",
    },
    update: {},
    },
    }
    1. It uses default implementation for getAll, get, and update,
    2. ... while specifying a custom implementation for create.
    3. getAll will be public (no auth needed), while the rest of the operations will be private.

    Here's what it looks like when visualized:

    Automatic CRUD with Wasp
    Visualization of the Tasks crud declaration

    We can now use the CRUD queries and actions we just specified in our client code.

    Keep reading for an example of Automatic CRUD in action, or skip ahead for the API Reference

    Example: A Simple TODO App

    Let's create a full-app example that uses automatic CRUD. We'll stick to using the Task entity from the previous example, but we'll add a User entity and enable username and password based auth.

    Automatic CRUD with Wasp
    We are building a simple tasks app with username based auth

    Creating the App

    We can start by running wasp new tasksCrudApp and then adding the following to the main.wasp file:

    main.wasp
    app tasksCrudApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "Tasks Crud App",

    // We enabled auth and set the auth method to username and password
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    tasks Task[]
    psl=}

    // We defined a Task entity on which we'll enable CRUD later on
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    psl=}

    // Tasks app routes
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@src/MainPage.jsx",
    authRequired: true,
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage.jsx",
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage.jsx",
    }

    We can then run wasp db migrate-dev to create the database and run the migrations.

    Adding CRUD to the Task Entity ✨

    Let's add the following crud declaration to our main.wasp file:

    main.wasp
    // ...

    crud Tasks {
    entity: Task,
    operations: {
    getAll: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js",
    },
    },
    }

    You'll notice that we enabled only getAll and create operations. This means that only these operations will be available.

    We also overrode the create operation with a custom implementation. This means that the create operation will not be generated, but instead, the createTask function from @src/tasks.js will be used.

    Our Custom create Operation

    Here's the src/tasks.ts file:

    src/tasks.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    We made a custom create operation because we want to make sure that the task is connected to the user that is creating it. Automatic CRUD doesn't support this by default (yet!). Read more about the default implementations here.

    Using the Generated CRUD Operations on the Client

    And let's use the generated operations in our client code:

    src/MainPage.jsx
    import { Tasks } from 'wasp/client/crud'
    import { useState } from 'react'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
    const createTask = Tasks.create.useAction()
    const [taskDescription, setTaskDescription] = useState('')

    function handleCreateTask() {
    createTask({ description: taskDescription, isDone: false })
    setTaskDescription('')
    }

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    return (
    <div
    style={{
    fontSize: '1.5rem',
    display: 'grid',
    placeContent: 'center',
    height: '100vh',
    }}
    >
    <div>
    <input
    value={taskDescription}
    onChange={(e) => setTaskDescription(e.target.value)}
    />
    <button onClick={handleCreateTask}>Create task</button>
    </div>
    <ul>
    {tasks.map((task) => (
    <li key={task.id}>{task.description}</li>
    ))}
    </ul>
    </div>
    )
    }

    And here are the login and signup pages, where we are using Wasp's Auth UI components:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function LoginPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <LoginForm />
    <div>
    <Link to="/signup">Create an account</Link>
    </div>
    </div>
    )
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    export function SignupPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <SignupForm />
    </div>
    )
    }

    That's it. You can now run wasp start and see the app in action. ⚡️

    You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.

    Future of CRUD Operations in Wasp

    CRUD operations currently have a limited set of knowledge about the business logic they are implementing.

    • For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the create operation in the example above.
    • Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
    • Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.

    CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.

    We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on this GitHub issue to see how we are doing.

    API Reference

    CRUD declaration work on top of existing entity declaration. We'll fully explore the API using two examples:

    1. A basic CRUD declaration that relies on default options.
    2. A more involved CRUD declaration that uses extra options and overrides.

    Declaring a CRUD With Default Options

    If we create CRUD operations for an entity named Task, like this:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    get: {},
    getAll: {},
    create: {},
    update: {},
    delete: {},
    },
    }

    Wasp will give you the following default implementations:

    get - returns one entity based on the id field

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.findUnique({ where: { id: args.id } })

    getAll - returns all entities

    // ...

    // If the operation is not public, Wasp checks if an authenticated user
    // is making the request.

    return Task.findMany()

    create - creates a new entity

    // ...
    return Task.create({ data: args.data })

    update - updates an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.update({ where: { id: args.id }, data: args.data })

    delete - deletes an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.delete({ where: { id: args.id } })
    Current Limitations

    In the default create and update implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.

    In the future, we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.

    For now, the solution is to provide an override function. You can override the default implementation by using the overrideFn option and implementing the validation logic yourself.

    Declaring a CRUD With All Available Options

    Here's an example of a more complex CRUD declaration:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // optional, defaults to false
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js", // optional
    },
    update: {},
    },
    }

    The CRUD declaration features the following fields:

    • entity: Entity required

      The entity to which the CRUD operations will be applied.

    • operations: { [operationName]: CrudOperationOptions } required

      The operations to be generated. The key is the name of the operation, and the value is the operation configuration.

      • The possible values for operationName are:
        • getAll
        • get
        • create
        • update
        • delete
      • CrudOperationOptions can have the following fields:
        • isPublic: bool - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to false.
        • overrideFn: ExtImport - The import statement of the optional override implementation in Node.js.

    Defining the overrides

    Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:

    • args

      The arguments of the operation i.e. the data sent from the client.

    • context

      Context contains the user making the request and the entities object with the entity that's being operated on.

    For a usage example, check the example guide.

    Using the CRUD operations in client code

    On the client, you import the CRUD operations from wasp/client/crud by import the {crud name} object. For example, if you have a CRUD called Tasks, you would import the operations like this:

    SomePage.jsx
    import { Tasks } from 'wasp/client/crud'

    You can then access the operations like this:

    SomePage.jsx
    const { data } = Tasks.getAll.useQuery()
    const { data } = Tasks.get.useQuery({ id: 1 })
    const createAction = Tasks.create.useAction()
    const updateAction = Tasks.update.useAction()
    const deleteAction = Tasks.delete.useAction()

    All CRUD operations are implemented with Queries and Actions under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)


    Join our community on Discord, where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/entities.html b/docs/0.13.0/data-model/entities.html index 92ce07ec27..00a4917884 100644 --- a/docs/0.13.0/data-model/entities.html +++ b/docs/0.13.0/data-model/entities.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. Wasp Entities directly correspond to Prisma's data model. Still, you don't need to be familiar with Prisma to effectively use Wasp, as it comes with a simple API wrapper for working with Prisma's core features.

    The only requirement for defining Wasp Entities is familiarity with the Prisma Schema Language (PSL), a simple definition language explicitly created for defining models in Prisma. The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:

    Defining an Entity

    As mentioned, an entity declaration represents a database model.

    Each Entity declaration corresponds 1-to-1 to Prisma's data model. Here's how you could define an Entity that represents a Task:

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}

    Let's go through this declaration in detail:

    • entity Task - This tells Wasp that we wish to define an Entity (i.e., database model) called Task. Wasp automatically creates a table called tasks.
    • {=psl ... psl=} - Wasp treats everything that comes between the two psl tags as PSL (Prisma Schema Language).

    The above PSL definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the tasks table has three columns):

    • id - An integer value serving as a primary key. The database automatically generates it by incrementing the previously generated id.
    • description - A string value for storing the task's description.
    • isDone - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to false by default.

    Working with Entities

    Let's see how you can define and work with Wasp Entities:

    1. Create/update some Entities in your .wasp file.
    2. Run wasp db migrate-dev. This command syncs the database model with the Entity definitions in your .wasp file. It does this by creating migration scripts.
    3. Migration scripts are automatically placed in the migrations/ folder. Make sure to commit this folder into version control.
    4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about operations).

    Using Entities in Operations

    Most of the time, you will be working with Entities within the context of Operations (Queries & Actions). We'll see how that's done on the next page.

    Using Entities directly

    If you need more control, you can directly interact with Entities by importing and using the Prisma Client. We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.

    You can only use the Prisma Client in your Wasp server code. You can import it like this:

    import { prisma } from 'wasp/server'

    prisma.task.create({
    description: "Read the Entities doc",
    isDone: true // almost :)
    })

    Next steps

    Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/operations/actions.html b/docs/0.13.0/data-model/operations/actions.html index 3c90334518..09aa8cca7b 100644 --- a/docs/0.13.0/data-model/operations/actions.html +++ b/docs/0.13.0/data-model/operations/actions.html @@ -19,8 +19,8 @@ - - + +
    @@ -42,7 +42,7 @@ Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

    1. args (type depends on the Action)

      An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

    2. context (type depends on the Action)

      An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

    Example

    The following Action:

    action createFoo {
    fn: import { createFoo } from "@src/actions.js"
    entities: [Foo]
    }

    Expects to find a named export createfoo from the file src/actions.js

    actions.js
    export const createFoo = (args, context) => {
    // implementation
    }

    The useAction Hook and Optimistic Updates

    Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

    When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

    The useAction hook accepts two arguments:

    • actionFn required

      The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

    • actionOptions

      An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

      • optimisticUpdates

        An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

        • getQuerySpecifier required

        A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).

        • updateQuery required

        The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

        • item - The argument you pass into the decorated Action.
        • oldData - The currently cached value for the Query identified by the specifier.
    caution

    The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

    Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

    Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

    Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

    src/pages/Task.jsx
    import React from 'react'
    import {
    useQuery,
    useAction,
    getTask,
    markTaskAsDone,
    } from 'wasp/client/operations'

    const TaskPage = ({ id }) => {
    const { data: task } = useQuery(getTask, { id })
    const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
    optimisticUpdates: [
    {
    getQuerySpecifier: ({ id }) => [getTask, { id }],
    updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
    },
    ],
    })

    if (!task) {
    return <h1>"Loading"</h1>
    }

    const { description, isDone } = task
    return (
    <div>
    <p>
    <strong>Description: </strong>
    {description}
    </p>
    <p>
    <strong>Is done: </strong>
    {isDone ? 'Yes' : 'No'}
    </p>
    {isDone || (
    <button onClick={() => markTaskAsDoneOptimistically({ id })}>
    Mark as done.
    </button>
    )}
    </div>
    )
    }

    export default TaskPage

    Advanced usage

    The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

    Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

    If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

    import { getTasks } from 'wasp/client/operations'

    const queryKey = getTasks.queryCacheKey
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/operations/overview.html b/docs/0.13.0/data-model/operations/overview.html index eaa97707a5..830dd435e9 100644 --- a/docs/0.13.0/data-model/operations/overview.html +++ b/docs/0.13.0/data-model/operations/overview.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Overview

    While Entities enable help you define your app's data model and relationships, Operations are all about working with this data.

    There are two kinds of Operations: Queries and Actions. As their names suggest, Queries are meant for reading data, and Actions are meant for changing it (either by updating existing entries or creating new ones).

    Keep reading to find out all there is to know about Operations in Wasp.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/data-model/operations/queries.html b/docs/0.13.0/data-model/operations/queries.html index 6d35e8ba16..957a4135ff 100644 --- a/docs/0.13.0/data-model/operations/queries.html +++ b/docs/0.13.0/data-model/operations/queries.html @@ -19,8 +19,8 @@ - - + +
    @@ -46,7 +46,7 @@ behavior for this particular Query. If you want to change the global defaults, you can do so in the client setup function.

    For an example of usage, check this section.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/editor-setup.html b/docs/0.13.0/editor-setup.html index 59387386f5..0bdbfd7075 100644 --- a/docs/0.13.0/editor-setup.html +++ b/docs/0.13.0/editor-setup.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/general/cli.html b/docs/0.13.0/general/cli.html index caa0b49d65..47904be16c 100644 --- a/docs/0.13.0/general/cli.html +++ b/docs/0.13.0/general/cli.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    new:ai <app-name> <app-description> [<config-json>]
    Uses AI to create a new Wasp project just based on the app name and the description.
    You can do the same thing with `wasp new` interactively.
    Run `wasp new:ai` for more info.

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code, all cached artifacts, and the node_modules dir.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about the current Wasp project.
    test Executes tests in your project.
    studio (experimental) GUI for inspecting your Wasp app.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      Simple starter template with a single page.
      [2] todo-ts
      Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
      [3] saas
      Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
      [4] embeddings
      Comes with code for generating vector embeddings and performing vector similarity search.
      [5] ai-generated
      🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
      ▸ 1

      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      🐝 --- Deleting the .wasp/ directory... -------------------------------------------

      ✅ --- Deleted the .wasp/ directory. ----------------------------------------------

      🐝 --- Deleting the node_modules/ directory... ------------------------------------

      ✅ --- Deleted the node_modules/ directory. ---------------------------------------
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    • wasp studio shows you an graphical overview of your application in a graph: pages, queries, actions, data model etc.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.12.0

      If you wish to install/switch to the latest version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s

      If you want specific x.y.z version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v x.y.z

      Check https://github.com/wasp-lang/wasp/releases for the list of valid versions, including the latest one.
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/general/language.html b/docs/0.13.0/general/language.html index 43c5cc3cfd..9c3fa8b246 100644 --- a/docs/0.13.0/general/language.html +++ b/docs/0.13.0/general/language.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Wasp Language (.wasp)

    Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

    It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

    It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

    Declarations

    The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

    app MyApp {
    title: "My app"
    }

    route RootRoute { path: "/", to: DashboardPage }

    page DashboardPage {
    component: import { DashboardPage } from "@src/Dashboard.jsx"
    }

    In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

    Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

    • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
    • <declaration_name> is an identifier chosen by you to name this specific declaration
    • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

    So, for app declaration above, we have:

    • declaration type app
    • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
    • declaration body { title: "My app" }, which is a dictionary with field title that has string value. Type of this dictionary is in line with the declaration body type of the app declaration type. If we provided something else, e.g. changed title to little, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for app.

    Each declaration has a meaning behind it that describes how your web app should behave and function.

    All the other types in Wasp language (primitive types (string, number), composite types (dict, list), enum types (DbSystem), ...) are used to define the declaration bodies.

    Complete List of Wasp Types

    Wasp's type system can be divided into two main categories of types: fundamental types and domain types.

    While fundamental types are here to be basic building blocks of a language and are very similar to what you would see in other popular languages, domain types are what make Wasp special, as they model the concepts of a web app like page, route and similar.

    • Fundamental types (source of truth)
      • Primitive types
        • string ("foo", "they said: \"hi\"")
        • bool (true, false)
        • number (12, 14.5)
        • declaration reference (name of existing declaration: TaskPage, updateTask)
        • ExtImport (external import) (import Foo from "@src/bar.js", import { Smth } from "@src/a/b.js")
          • The path has to start with "@src". The rest is relative to the src directory.
          • Import has to be a default import import Foo or a single named import import { Foo }.
        • json ({=json { a: 5, b: ["hi"] } json=})
        • psl (Prisma Schema Language) ({=psl <psl data model syntax> psl=})
      • Composite types
        • dict (dictionary) ({ a: 5, b: "foo" })
        • list ([1, 2, 3])
        • tuple ((1, "bar"), (2, 4, true))
          • Tuples can be of size 2, 3 and 4.
    • Domain types (source of truth)
      • Declaration types
        • action
        • api
        • apiNamespace
        • app
        • entity
        • job
        • page
        • query
        • route
        • crud
      • Enum types
        • DbSystem
        • HttpMethod
        • JobExecutor
        • EmailProvider

    You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/migrate-from-0-11-to-0-12.html b/docs/0.13.0/migrate-from-0-11-to-0-12.html index f6b82c4f37..41e7fb2519 100644 --- a/docs/0.13.0/migrate-from-0-11-to-0-12.html +++ b/docs/0.13.0/migrate-from-0-11-to-0-12.html @@ -19,8 +19,8 @@ - - + +
    @@ -56,7 +56,7 @@ src/server), you are now free to reorganize your project however you think is best, as long as you keep all the source files in the src/ directory.

    This section is optional, but if you didn't like the server/client separation, now's the perfect time to change it.

    For example, if your src dir looked like this:

    src

    ├── client
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── MainPage.tsx
    │   ├── Register.tsx
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   ├── Task.tsx
    │   └── User.tsx
    ├── server
    │   ├── taskActions.ts
    │   ├── taskQueries.ts
    │   ├── userActions.ts
    │   └── userQueries.ts
    └── shared
    └── utils.ts

    you can now change it to a feature-based structure (which we recommend for any project that is not very small):

    src

    ├── task
    │   ├── actions.ts -- former taskActions.ts
    │   ├── queries.ts -- former taskQueries.ts
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   └── Task.tsx
    ├── user
    │   ├── actions.ts -- former userActions.ts
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── queries.ts -- former userQueries.ts
    │   ├── Register.tsx
    │   └── User.tsx
    ├── MainPage.tsx
    └── utils.ts

    Appendix

    Example Data Migration Functions

    The migration functions provided below are written with the typical use cases in mind and you can use them as-is. If your setup requires additional logic, you can use them as a good starting point and modify them to your needs.

    Note that all of the functions below are written to be idempotent, meaning that running a function multiple times can't hurt. This allows executing a function again in case only a part of the previous execution succeeded and also means that accidentally running it one time too much won't have any negative effects. We recommend you keep your data migration functions idempotent.

    Username & Password

    To successfully migrate the users using the Username & Password auth method, you will need to do two things:

    1. Migrate the user data

      Username & Password data migration function
      main.wasp
      api migrateUsernameAndPassword {
      httpRoute: (GET, "/migrate-username-and-password"),
      fn: import { migrateUsernameAndPasswordHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type UsernameProviderData } from "wasp/server/auth";
      import { MigrateUsernameAndPassword } from "wasp/server/api";

      export const migrateUsernameAndPasswordHandler: MigrateUsernameAndPassword =
      async (_req, res) => {
      const result = await migrateUsernameAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateUsernameAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.username || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using username auth) with id:", user.id);
      continue;
      }

      const providerData: UsernameProviderData = {
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "username";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.username.toLowerCase(),
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Provide a way for users to migrate their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to migrate their password after the migration, as the old password will no longer work.

      Since the only way users using username and password as a login method can verify their identity is by providing both their username and password (there is no email or any other info, unless you asked for it and stored it explicitly), we need to provide them a way to exchange their old password for a new password. One way to handle this is to inform them about the need to migrate their password (on the login page) and provide a custom page to migrate the password.

    Steps to create a custom page for migrating the password
    1. You will need to install the secure-password and sodium-native packages to use the old hashing algorithm:

      npm install secure-password@4.0.0 sodium-native@3.3.0 --save-exact

      Make sure to save the exact versions of the packages.

    2. Then you'll need to create a new page in your app where users can migrate their password. You can use the following code as a starting point:

    main.wasp
    route MigratePasswordRoute { path: "/migrate-password", to: MigratePassword }
    page MigratePassword {
    component: import { MigratePasswordPage } from "@src/pages/MigratePassword"
    }
    src/pages/MigratePassword.jsx
    import {
    FormItemGroup,
    FormLabel,
    FormInput,
    FormError,
    } from "wasp/client/auth";
    import { useForm } from "react-hook-form";
    import { migratePassword } from "wasp/client/operations";
    import { useState } from "react";

    export function MigratePasswordPage() {
    const [successMessage, setSuccessMessage] = useState(null);
    const [errorMessage, setErrorMessage] = useState(null);
    const form = useForm();

    const onSubmit = form.handleSubmit(async (data) => {
    try {
    const result = await migratePassword(data);
    setSuccessMessage(result.message);
    } catch (e) {
    console.error(e);
    if (e instanceof Error) {
    setErrorMessage(e.message);
    }
    }
    });

    return (
    <div style={{
    maxWidth: "400px",
    margin: "auto",
    }}>
    <h1>Migrate your password</h1>
    <p>
    If you have an account on the old version of the website, you can
    migrate your password to the new version.
    </p>
    {successMessage && <div>{successMessage}</div>}
    {errorMessage && <FormError>{errorMessage}</FormError>}
    <form onSubmit={onSubmit}>
    <FormItemGroup>
    <FormLabel>Username</FormLabel>
    <FormInput
    {...form.register("username", {
    required: "Username is required",
    })}
    />
    <FormError>{form.formState.errors.username?.message}</FormError>
    </FormItemGroup>
    <FormItemGroup>
    <FormLabel>Password</FormLabel>
    <FormInput
    {...form.register("password", {
    required: "Password is required",
    })}
    type="password"
    />
    <FormError>{form.formState.errors.password?.message}</FormError>
    </FormItemGroup>
    <button type="submit">Migrate password</button>
    </form>
    </div>
    );
    }
    1. Finally, you will need to create a new operation in your app to handle the password migration. You can use the following code as a starting point:
    main.wasp
    action migratePassword {
    fn: import { migratePassword } from "@src/auth",
    entities: []
    }
    src/auth.js
    import SecurePassword from "secure-password";
    import { HttpError } from "wasp/server";
    import {
    createProviderId,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    } from "wasp/server/auth";

    export const migratePassword = async ({ password, username }, _context) => {
    const providerId = createProviderId("username", username);
    const authIdentity = await findAuthIdentity(providerId);

    if (!authIdentity) {
    throw new HttpError(400, "Something went wrong");
    }

    const providerData = deserializeAndSanitizeProviderData(
    authIdentity.providerData
    );

    try {
    const SP = new SecurePassword();

    // This will verify the password using the old algorithm
    const result = await SP.verify(
    Buffer.from(password),
    Buffer.from(providerData.hashedPassword, "base64")
    );

    if (result !== SecurePassword.VALID) {
    throw new HttpError(400, "Something went wrong");
    }

    // This will hash the password using the new algorithm and update the
    // provider data in the database.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: password,
    });
    } catch (e) {
    throw new HttpError(400, "Something went wrong");
    }

    return {
    message: "Password migrated successfully.",
    };
    };

    Email

    To successfully migrate the users using the Email auth method, you will need to do two things:

    1. Migrate the user data

      Email data migration function
      main.wasp
      api migrateEmail {
      httpRoute: (GET, "/migrate-email"),
      fn: import { migrateEmailHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type EmailProviderData } from "wasp/server/auth";
      import { MigrateEmail } from "wasp/server/api";

      export const migrateEmailHandler: MigrateEmail =
      async (_req, res) => {
      const result = await migrateEmailAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateEmailAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.email || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using email auth) with id:", user.id);
      continue;
      }

      const providerData: EmailProviderData = {
      isEmailVerified: user.isEmailVerified,
      emailVerificationSentAt:
      user.emailVerificationSentAt?.toISOString() ?? null,
      passwordResetSentAt: user.passwordResetSentAt?.toISOString() ?? null,
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "email";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.email,
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Ask the users to reset their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to reset their password after the migration, as the old password will no longer work.

      It would be best to notify your users about this change and put a notice on your login page to request a password reset.

    Google & GitHub

    Google & GitHub data migration functions
    main.wasp
    api migrateGoogle {
    httpRoute: (GET, "/migrate-google"),
    fn: import { migrateGoogleHandler } from "@src/migrateToNewAuth",
    entities: []
    }

    api migrateGithub {
    httpRoute: (GET, "/migrate-github"),
    fn: import { migrateGithubHandler } from "@src/migrateToNewAuth",
    entities: []
    }
    src/migrateToNewAuth.ts
    import { prisma } from "wasp/server";
    import { MigrateGoogle, MigrateGithub } from "wasp/server/api";

    export const migrateGoogleHandler: MigrateGoogle =
    async (_req, res) => {
    const result = await createSocialLoginMigration("google");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    export const migrateGithubHandler: MigrateGithub =
    async (_req, res) => {
    const result = await createSocialLoginMigration("github");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    async function createSocialLoginMigration(
    providerName: "google" | "github"
    ): Promise<{
    numUsersAlreadyMigrated: number;
    numUsersNotUsingThisAuthMethod: number;
    numUsersMigratedSuccessfully: number;
    }> {
    const users = await prisma.user.findMany({
    include: {
    auth: true,
    externalAuthAssociations: true,
    },
    });

    const result = {
    numUsersAlreadyMigrated: 0,
    numUsersNotUsingThisAuthMethod: 0,
    numUsersMigratedSuccessfully: 0,
    };

    for (const user of users) {
    if (user.auth) {
    result.numUsersAlreadyMigrated++;
    console.log("Skipping user (already migrated) with id:", user.id);
    continue;
    }

    const provider = user.externalAuthAssociations.find(
    (provider) => provider.provider === providerName
    );

    if (!provider) {
    result.numUsersNotUsingThisAuthMethod++;
    console.log(`Skipping user (not using ${providerName} auth) with id:`, user.id);
    continue;
    }

    await prisma.auth.create({
    data: {
    identities: {
    create: {
    providerName,
    providerUserId: provider.providerId,
    providerData: JSON.stringify({}),
    },
    },
    user: {
    connect: {
    id: user.id,
    },
    },
    },
    });
    result.numUsersMigratedSuccessfully++;
    }

    return result;
    }
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/migrate-from-0-12-to-0-13.html b/docs/0.13.0/migrate-from-0-12-to-0-13.html index c1ff6b766f..21fb381cc2 100644 --- a/docs/0.13.0/migrate-from-0-12-to-0-13.html +++ b/docs/0.13.0/migrate-from-0-12-to-0-13.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Migration from 0.12.X to 0.13.X

    Are you on 0.11.X or earlier?

    This guide only covers the migration from 0.12.X to 0.13.X. If you are migrating from 0.11.X or earlier, please read the migration guide from 0.11.X to 0.12.X first.

    What's new in 0.13.0?

    OAuth providers got an overhaul

    Wasp 0.13.0 switches away from using Passport for our OAuth providers in favor of Arctic from the Lucia ecosystem. This change simplifies the codebase and makes it easier to add new OAuth providers in the future.

    We added Keycloak as an OAuth provider

    Wasp now supports using Keycloak as an OAuth provider.

    How to migrate?

    Migrate your OAuth setup

    We had to make some breaking changes to upgrade the OAuth setup to the new Arctic lib.

    Follow the steps below to migrate:

    1. Define the WASP_SERVER_URL server env variable

      In 0.13.0 Wasp introduces a new server env variable WASP_SERVER_URL that you need to define. This is the URL of your Wasp server and it's used to generate the redirect URL for the OAuth providers.

      Server env variables
      WASP_SERVER_URL=https://your-wasp-server-url.com

      In development, Wasp sets the WASP_SERVER_URL to http://localhost:3001 by default.

      Migrating a deployed app

      If you are migrating a deployed app, you will need to define the WASP_SERVER_URL server env variable in your deployment environment.

      Read more about setting env variables in production here.

    2. Update the redirect URLs for the OAuth providers

      The redirect URL for the OAuth providers has changed. You will need to update the redirect URL for the OAuth providers in the provider's dashboard.

      {clientUrl}/auth/login/{provider}

      Check the new redirect URLs for Google and GitHub in Wasp's docs.

    3. Update the configFn for the OAuth providers

      If you didn't use the configFn option, you can skip this step.

      If you used the configFn to configure the scope for the OAuth providers, you will need to rename the scope property to scopes.

      Also, the object returned from configFn no longer needs to include the Client ID and the Client Secret. You can remove them from the object that configFn returns.

      google.ts
      export function getConfig() {
      return {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      scope: ['profile', 'email'],
      }
      }
    4. Update the userSignupFields fields to use the new profile format

      If you didn't use the userSignupFields option, you can skip this step.

      The data format for the profile that you receive from the OAuth providers has changed. You will need to update your code to reflect this change.

      google.ts
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      displayName: (data: any) => data.profile.displayName,
      })

      Wasp now directly forwards what it receives from the OAuth providers. You can check the data format for Google and GitHub in Wasp's docs.

    That's it!

    You should now be able to run your app with the new Wasp 0.13.0.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/client-config.html b/docs/0.13.0/project/client-config.html index d7504956d7..beb0306f81 100644 --- a/docs/0.13.0/project/client-config.html +++ b/docs/0.13.0/project/client-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -35,7 +35,7 @@ renders a custom layout:

    src/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return (
    <Provider store={store}>
    <Layout>{children}</Layout>
    </Provider>
    )
    }

    function Layout({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }
  • setupFn: ExtImport

    You can use this function to perform any custom setup (e.g., setting up client-side periodic jobs).

    src/myClientSetupCode.js
    export default async function mySetupFunction() {
    // Run some code
    }
  • baseDir: String

    If you need to serve the client from a subdirectory, you can use the baseDir option.

    If you set baseDir to /my-app for example, that will make Wasp set the basename prop of the Router to /my-app. It will also set the base option of the Vite config to /my-app.

    This means that if you serve your app from https://example.com/my-app, the router will work correctly, and all the assets will be served from https://example.com/my-app.

    Setting the correct env variable

    If you set the baseDir option, make sure that the WASP_WEB_CLIENT_URL env variable also includes that base directory.

    For example, if you are serving your app from https://example.com/my-app, the WASP_WEB_CLIENT_URL should be also set to https://example.com/my-app, and not just https://example.com.

  • - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/css-frameworks.html b/docs/0.13.0/project/css-frameworks.html index 650ad9d1e5..f50ebdbedd 100644 --- a/docs/0.13.0/project/css-frameworks.html +++ b/docs/0.13.0/project/css-frameworks.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── package.json
    ├── src
    │   ├── Main.css
    │   ├── MainPage.jsx
    │   ├── vite-env.d.ts
    │   └── waspLogo.png
    ├── public
    ├── tsconfig.json
    ├── vite.config.ts
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      const { resolveProjectPath } = require('wasp/dev')

      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [resolveProjectPath('./src/**/*.{js,jsx,ts,tsx}')],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, install them as npm development dependencies and add them to the plugins list in your tailwind.config.cjs file:

    npm install -D @tailwindcss/forms
    npm install -D @tailwindcss/typography

    and also

    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/custom-vite-config.html b/docs/0.13.0/project/custom-vite-config.html index bcb77cda76..6cc968dfc6 100644 --- a/docs/0.13.0/project/custom-vite-config.html +++ b/docs/0.13.0/project/custom-vite-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Custom Vite Config

    Wasp uses Vite to serve the client during development and bundling it for production. If you want to customize the Vite config, you can do that by editing the vite.config.ts file in your project root directory.

    Wasp will use your config and merge it with the default Wasp's Vite config.

    Vite config customization can be useful for things like:

    • Adding custom Vite plugins.
    • Customising the dev server.
    • Customising the build process.

    Be careful with making changes to the Vite config, as it can break the Wasp's client build process. Check out the default Vite config here to see what you can change.

    Examples

    Below are some examples of how you can customize the Vite config.

    Changing the Dev Server Behaviour

    If you want to stop Vite from opening the browser automatically when you run wasp start, you can do that by customizing the open option.

    vite.config.js
    export default {
    server: {
    open: false,
    },
    }

    Custom Dev Server Port

    You have access to all of the Vite dev server options in your custom Vite config. You can change the dev server port by setting the port option.

    vite.config.js
    export default {
    server: {
    port: 4000,
    },
    }
    .env.server
    WASP_WEB_CLIENT_URL=http://localhost:4000
    Changing the dev server port

    ⚠️ Be careful when changing the dev server port, you'll need to update the WASP_WEB_CLIENT_URL env var in your .env.server file.

    Customising the Base Path

    If you, for example, want to serve the client from a different path than /, you can do that by customizing the base option.

    vite.config.js
    export default {
    base: '/my-app/',
    }
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/customizing-app.html b/docs/0.13.0/project/customizing-app.html index 266d0fdf4d..dad5fb7373 100644 --- a/docs/0.13.0/project/customizing-app.html +++ b/docs/0.13.0/project/customizing-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Customizing the App

    Each Wasp project can have only one app type declaration. It is used to configure your app and its components.

    app todoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ]
    }

    We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the API Reference.

    Changing the App Title

    You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the title field of your app declaration:

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "BookFace"
    }

    Adding Additional Lines to the Head

    If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the head field of your app declaration.

    An example of adding extra style sheets and scripts:

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    head: [ // optional
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
    "<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
    ]
    }

    API Reference

    app todoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ],
    auth: {
    // ...
    },
    client: {
    // ...
    },
    server: {
    // ...
    },
    db: {
    // ...
    },
    emailSender: {
    // ...
    },
    webSocket: {
    // ...
    }
    }

    The app declaration has the following fields:

    • wasp: dict required Wasp compiler configuration. It is a dictionary with a single field:

      • version: string required

        The version specifies which versions of Wasp are compatible with the app. It should contain a valid SemVer range

        info

        For now, the version field only supports caret ranges (i.e., ^x.y.z). Support for the full specification will come in a future version of Wasp

    • title: string required

      Title of your app. It will appear in the browser tab, next to the favicon.

    • head: [string]

      List of additional lines (e.g. <link> or <script> tags) to be included in the <head> of your HTML document.

    The rest of the fields are covered in dedicated sections of the docs:

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/dependencies.html b/docs/0.13.0/project/dependencies.html index 5a55b6403b..7e8ac53ae1 100644 --- a/docs/0.13.0/project/dependencies.html +++ b/docs/0.13.0/project/dependencies.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Dependencies

    In a Wasp project, dependencies are defined in a standard way for JavaScript projects: using the package.json file, located at the root of your project. You can list your dependencies under the dependencies or devDependencies fields.

    Adding a New Dependency

    To add a new package, like date-fns (a great date handling library), you use npm:

    npm install date-fns

    This command will add the package in the dependencies section of your package.json file.

    You will notice that there are some other packages in the dependencies section, like react and wasp. These are the packages that Wasp uses internally, and you should not modify or remove them.

    Using Packages that are Already Used by Wasp Internally

    In the current version of Wasp, if Wasp is already internally using a certain dependency (e.g. React) with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version.

    If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp dictates exact versions of certain packages, so for example you can't choose the version of React you want to use.

    note

    We are currently working on a restructuring that will solve this and some other quirks: check issue #734 to follow our progress.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/env-vars.html b/docs/0.13.0/project/env-vars.html index c730b6410a..4f642b60a5 100644 --- a/docs/0.13.0/project/env-vars.html +++ b/docs/0.13.0/project/env-vars.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Env Variables

    Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.

    For instance, during development, you may want your project to connect to a local development database running on your machine, but in production, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.

    While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.

    In Wasp, you can use environment variables in both the client and the server code.

    Client Env Vars

    Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    To enable Wasp to pick them up, client environment variables must be prefixed with REACT_APP_, for example: REACT_APP_SOME_VAR_NAME=....

    You can read them from the client code like this:

    src/App.js
    console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)

    Check below on how to define them.

    Server Env Vars

    In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as SOME_VAR_NAME=....

    You can read them in the server code like this:

    console.log(process.env.SOME_VAR_NAME)

    Check below on how to define them.

    Defining Env Vars in Development

    During development, there are two ways to provide env vars to your Wasp project:

    1. Using .env files. (recommended)
    2. Using shell. (useful for overrides)

    1. Using .env (dotenv) Files

    Env vars usage in development

    This is the recommended method for providing env vars to your Wasp project during development.

    In the root of your Wasp project you can create two distinct files:

    • .env.server for env vars that will be provided to the server.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.server
      DATABASE_URL=postgresql://localhost:5432
      SOME_VAR_NAME=somevalue
    • .env.client for env vars that will be provided to the client.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.client
      REACT_APP_SOME_VAR_NAME=somevalue

    These files should not be committed to version control, and they are already ignored by default in the .gitignore file that comes with Wasp.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, or by defining them at the start of a command:

    SOME_VAR_NAME=SOMEVALUE wasp start

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    You should provide them to the build command, for example:

    REACT_APP_SOME_VAR_NAME=somevalue npm run build
    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME with the value you provided. This is done during the build process, so the value is embedded into the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/server-config.html b/docs/0.13.0/project/server-config.html index 3f6ad454a5..314745fcdf 100644 --- a/docs/0.13.0/project/server-config.html +++ b/docs/0.13.0/project/server-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ExtImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server logic.

      src/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/starter-templates.html b/docs/0.13.0/project/starter-templates.html index 7c38cfc6b0..687284881a 100644 --- a/docs/0.13.0/project/starter-templates.html +++ b/docs/0.13.0/project/starter-templates.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    Simple starter template with a single page.
    [2] todo-ts
    Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
    [3] saas
    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
    [4] embeddings
    Comes with code for generating vector embeddings and performing vector similarity search.
    [5] ai-generated
    🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
    ▸ 1

    🐝 --- Creating your project from the "basic" template... -------------------------

    Created new Wasp app in ./MyFirstProject directory!

    To run your new app, do:
    cd MyFirstProject
    wasp db start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    OpenSaaS.sh template

    SaaS Template

    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.

    Features: Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Full-stack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    Todo App w/ Typescript

    A simple Todo App with Typescript and Full-stack Type Safety.

    Features: Auth (username/password), Full-stack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts

    AI Generated Starter 🤖

    Using the same tech as used on https://usemage.ai/, Wasp generates your custom starter template based on your project description. It will automatically generate your data model, auth, queries, actions and React pages.

    You will need to provide your own OpenAI API key to be able to use this template.

    Features: Generated using OpenAI's GPT models, Auth (username/password), Queries, Actions, Pages, Full-stack Type Safety

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/static-assets.html b/docs/0.13.0/project/static-assets.html index 8bad395317..b3459004e7 100644 --- a/docs/0.13.0/project/static-assets.html +++ b/docs/0.13.0/project/static-assets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Static Asset Handling

    Importing an Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in the public directory at the root of your project:

    .
    └── public
    ├── favicon.ico
    └── robots.txt

    Assets in this directory will be served at root path / during development and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path
      • for example, public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/project/testing.html b/docs/0.13.0/project/testing.html index 459def31e3..e01969e6f6 100644 --- a/docs/0.13.0/project/testing.html +++ b/docs/0.13.0/project/testing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a real-time UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "wasp/client/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "wasp/client/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import { getTasks } from "wasp/client/operations";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "wasp/client";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/Todo.jsx
    import { useQuery, getTasks } from "wasp/client/operations";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import { getTasks } from "wasp/client/operations";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/Todo.jsx
    import { api } from "wasp/client/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    - - + + \ No newline at end of file diff --git a/docs/0.13.0/quick-start.html b/docs/0.13.0/quick-start.html index 0b0b2f8e0f..06ded356c3 100644 --- a/docs/0.13.0/quick-start.html +++ b/docs/0.13.0/quick-start.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Quick Start

    Installation

    Welcome, new Waspeteer 🐝!

    Let's create and run our first Wasp app in 3 short steps:

    1. To install Wasp on Linux / OSX / WSL (Windows), open your terminal and run:

      curl -sSL https://get.wasp-lang.dev/installer.sh | sh

      ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    2. Then, create a new app by running:

      wasp new
    3. Finally, run the app:

      cd <my-project-name>
      wasp start

    That's it 🎉 You have successfully created and served a new full-stack web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong with the installation, or if you have additional questions.

    Want an even faster start?

    Try out Wasp AI 🤖 to generate a new Wasp app in minutes just from a title and short description!

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser with GitHub Codespaces by following the intructions in our Tutorial App README

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. A version of Node.js must be >= 18.

    If you need it, we recommend using nvm for managing your Node.js installation version(s).

    A quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 20

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 20

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh
    Running Wasp on Mac with Mx chip (arm64)

    Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)? Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install Rosetta on your Mac if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal

    softwareupdate --install-rosetta

    Once Rosetta is installed, you should be able to run Wasp without any issues.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/telemetry.html b/docs/0.13.0/telemetry.html index 99f9c12596..50f8de84a0 100644 --- a/docs/0.13.0/telemetry.html +++ b/docs/0.13.0/telemetry.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.13.0

    Telemetry

    Overview

    The term telemetry refers to the collection of certain usage data to help improve the quality of a piece of software (in this case, Wasp).

    Our telemetry implementation is anonymized and very limited in its scope, focused on answering following questions:

    • How many people and how often: tried to install Wasp, use Wasp, have built a Wasp app, or have deployed one?
    • How many projects are created with Wasp?

    When and what is sent?

    • Information is sent via HTTPS request when wasp CLI command is invoked. Information is sent no more than twice in a period of 12 hours (sending is paused for 12 hours after last invocation, separately for wasp build command and for all other commands). Exact information as it is sent:
      {
      // Randomly generated, non-identifiable UUID representing a user.
      "distinct_id": "bf3fa7a8-1c11-4f82-9542-ec1a2d28786b",
      // Non-identifiable hash representing a project.
      "project_hash": "6d7e561d62b955d1",
      // True if command was `wasp build`, false otherwise.
      "is_build": true,
      // Captures `wasp deploy ...` args, but only those from the limited, pre-defined list of keywords.
      // Those are "fly", "setup", "create-db", "deploy" and "cmd". Everything else is ommited.
      "deploy_cmd_args": "fly;deploy",
      "wasp_version": "0.1.9.1",
      "os": "linux",
      // "CI" if running on CI, and whatever is the content of "WASP_TELEMETRY_CONTEXT" env var.
      // We use this to track when execution is happening in some special context, like on Gitpod, Replit or similar.
      "context": "CI"
      }
    • Information is also sent once via HTTPS request when wasp is installed via install.sh script. Exact information as it is sent:
      {
      // Randomly generated id.
      "distinct_id": "274701613078193779564259",
      "os": "linux"
      }

    Opting out

    You sharing the telemetry data with us means a lot to us, since it helps us understand how popular Wasp is, how it is being used, how the changes we are doing affect usage, how many new vs old users there are, and just in general how Wasp is doing. We look at these numbers every morning and they drive us to make Wasp better.

    However, if you wish to opt-out of telemetry, we understand! You can do so by setting the WASP_TELEMETRY_DISABLE environment variable to any value, e.g.:

    export WASP_TELEMETRY_DISABLE=1

    Future plans

    We don't have this implemented yet, but the next step will be to make telemetry go in two directions -> instead of just sending usage data to us, it will also at the same time check for any messages from our side (e.g. notification about new version of Wasp, or a security notice). Link to corresponding github issue.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/actions.html b/docs/0.13.0/tutorial/actions.html index 15b9fa89ca..be191e179f 100644 --- a/docs/0.13.0/tutorial/actions.html +++ b/docs/0.13.0/tutorial/actions.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    6. Modifying Data

    In the previous section, we learned about using Queries to fetch data and only briefly mentioned that Actions can be used to update the database. Let's learn more about Actions so we can add and update tasks in the database.

    We have to create:

    1. A Wasp Action that creates a new task.
    2. A React form that calls that Action when the user creates a task.

    Creating a New Action

    Creating an Action is very similar to creating a Query.

    Declaring an Action

    We must first declare the Action in main.wasp:

    main.wasp
    // ...

    action createTask {
    fn: import { createTask } from "@src/actions",
    entities: [Task]
    }

    Implementing an Action

    Let's now define a function for our createTask Action:

    src/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src directory.

    Invoking the Action on the Client

    Start by defining a form for creating new tasks.

    src/MainPage.jsx
    import { 
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    // ... MainPage, TaskView, TaskList ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike Queries, you can call Actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.

    All that's left now is adding this form to the page component:

    src/MainPage.jsx
    import {
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    // ... TaskView, TaskList, NewTaskForm ...

    Great work!

    You now have a form for creating new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser. You'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! Wasp handles these automatic updates under the hood.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp keeps all your queries in sync with any changes made through Actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done.

    We'll create a new Action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an Action named updateTask that receives the task's id and its isDone status. You can see our implementation below.

    Solution

    Declaring the Action in main.wasp:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@src/actions",
    entities: [Task]
    }

    Implementing the Action on the server:

    src/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    You can now call updateTask from the React component:

    src/MainPage.jsx
    // ...
    import {
    updateTask,
    createTask,
    getTasks,
    useQuery,
    } from 'wasp/client/operations'

    // ... MainPage ...

    const TaskView = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error) {
    window.alert('Error while updating task: ' + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ... TaskList, NewTaskForm ...

    Awesome! You can now mark this task as done.

    It's time to make one final addition to your app: supporting multiple users.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/auth.html b/docs/0.13.0/tutorial/auth.html index bea5ba504f..51b5913a11 100644 --- a/docs/0.13.0/tutorial/auth.html +++ b/docs/0.13.0/tutorial/auth.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    7. Adding Authentication

    Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support.

    To add users to your app, you must:

    • Create a User Entity.
    • Tell Wasp to use the Username and Password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify your Queries and Actions so users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it will create the auth related entities for you in the background. Nothing to do here!

    You must only add the User Entity to keep track of who owns which tasks.

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    psl=}

    Adding Auth to the Project

    Next, tell Wasp to use full-stack authentication:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "TodoApp",
    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used in a bit.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    Don't forget to update the database schema by running:

    wasp db migrate-dev

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage"
    }

    Great, Wasp now knows these pages exist!

    Here's the React code for the pages you've just imported:

    src/LoginPage.jsx
    import { Link } from 'react-router-dom'
    import { LoginForm } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <LoginForm />
    <br />
    <span>
    I don't have an account yet (<Link to="/signup">go to signup</Link>).
    </span>
    </div>
    )
    }

    The signup page is very similar to the login page:

    src/SignupPage.jsx
    import { Link } from 'react-router-dom'
    import { SignupForm } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <SignupForm />
    <br />
    <span>
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </div>
    )
    }

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import { MainPage } from "@src/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/MainPage.jsx
    export const MainPage = ({ user }) => {
    // Do something with the user
    // ...
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    You'll notice that we now have a User entity in the database alongside the Task entity.

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    You might notice some extra Prisma models like Auth, AuthIdentity and Session that Wasp created for us. You don't need to care about these right now, but if you are curious, you can read more about them here.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    tasks Task[]
    psl=}

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User? @relation(fields: [userId], references: [id])
    userId Int?
    psl=}

    // ...

    As always, you must migrate the database after changing the Entities:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database.

    This isn't recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional.

    Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/queries.js
    import { HttpError } from 'wasp/server'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
    })
    }
    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/MainPage.jsx
    // ...
    import { logout } from 'wasp/client/auth'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/create.html b/docs/0.13.0/tutorial/create.html index 9bb14fcce4..1b055267d3 100644 --- a/docs/0.13.0/tutorial/create.html +++ b/docs/0.13.0/tutorial/create.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start
    note

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder page:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/entities.html b/docs/0.13.0/tutorial/entities.html index 2fcbfd4118..d6250eb910 100644 --- a/docs/0.13.0/tutorial/entities.html +++ b/docs/0.13.0/tutorial/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:

    main.wasp
    // ...

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}
    note

    Wasp uses Prisma as a way to talk to the database. You define entities by defining Prisma models using the Prisma Schema Language (PSL) between the {=psl psl=} tags.

    Read more in the Entities section of the docs.

    To update the database schema to include this entity, stop the wasp start process, if it's running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/pages.html b/docs/0.13.0/tutorial/pages.html index b6a4b43611..a1f639a1fb 100644 --- a/docs/0.13.0/tutorial/pages.html +++ b/docs/0.13.0/tutorial/pages.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the named export from src/MainPage.tsx.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    export const MainPage = () => {
    // ...
    }

    This is a regular functional React component. It also uses the CSS file and a logo image that sit next to it in the src folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    tip

    wasp start automatically picks up the changes you make and restarts the app, so keep it running in the background.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import { HelloPage } from "@src/HelloPage"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/HelloPage.tsx and pass the URL parameter the same way as in React Router:

    src/HelloPage.jsx
    export const HelloPage = (props) =>  {
    return <div>Here's {props.match.params.name}!</div>
    }

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Now that you've seen how Wasp deals with Routes and Pages, it's finally time to build the Todo app.

    Start by cleaning up the starter project and removing unnecessary code and files.

    First, remove most of the code from the MainPage component:

    src/MainPage.jsx
    export const MainPage = () => {
    return <div>Hello world!</div>
    }

    At this point, the main page should look like this:

    Todo App - Hello World

    You can now delete redundant files: src/Main.css, src/waspLogo.png, and src/HelloPage.tsx (we won't need this page for the rest of the tutorial).

    Since src/HelloPage.tsx no longer exists, remove its route and page declarations from the main.wasp file.

    Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "TodoApp"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@src/MainPage"
    }

    Excellent work!

    You now have a basic understanding of Wasp and are ready to start building your TodoApp. We'll implement the app's core features in the following sections.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/project-structure.html b/docs/0.13.0/tutorial/project-structure.html index c95c9f7a71..183ed2fa09 100644 --- a/docs/0.13.0/tutorial/project-structure.html +++ b/docs/0.13.0/tutorial/project-structure.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ We'll keep it simple by placing everything in the root src directory.

    Many other files (e.g., tsconfig.json, vite-env.d.ts, .wasproot, etc.) help Wasp and the IDE improve your development experience with autocompletion, IntelliSense, and error reporting.

    The vite.config.ts file is used to configure Vite, Wasp's build tool of choice. We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in custom Vite config docs.

    There's no need to spend more time discussing all the helper files. They'll silently do their job in the background and let you focus on building your app.

    TypeScript Support

    Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla JavaScript and TypeScript.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's take a closer look at main.wasp

    main.wasp

    main.wasp is your app's definition file. It defines the app's central components and helps Wasp to do a lot of the legwork for you.

    The file is a list of declarations. Each declaration defines a part of your app.

    The default main.wasp file generated with wasp new on the previous page looks like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.13.0" // Pins the version of Wasp to use.
    },
    title: "TodoApp" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that gets rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/tutorial/queries.html b/docs/0.13.0/tutorial/queries.html index 04576eb1ed..b01422d070 100644 --- a/docs/0.13.0/tutorial/queries.html +++ b/docs/0.13.0/tutorial/queries.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them!

    The primary way of working with Entities in Wasp is with Queries and Actions, collectively known as Operations.

    Queries are used to read an entity, while Actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a Query.

    To list the tasks, you must:

    1. Create a Query that fetches the tasks from the database.
    2. Update the MainPage.tsx to use that Query and display the results.

    Defining the Query

    We'll create a new Query called getTasks. We'll need to declare the Query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // The path `@src/queries` resolves to `src/queries.js`.
    // No need to specify an extension.
    fn: import { getTasks } from "@src/queries",
    // Tell Wasp that this query reads from the `Task` entity. Wasp will
    // automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'asc' },
    })
    }

    Query function parameters:

    • args: object

      The arguments the caller passes to the Query.

    • context

      An object with extra information injected by Wasp. Its type depends on the Query declaration.

    Since the Query declaration in main.wasp says that the getTasks Query uses Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and Actions are NodeJS functions executed on the server.

    Invoking the Query On the Frontend

    While we implement Queries on the server, Wasp generates client-side functions that automatically take care of serialization, network calls, and cache invalidation, allowing you to call the server code like it's a regular function.

    This makes it easy for us to use the getTasks Query we just created in our React component:

    src/MainPage.jsx
    import { getTasks, useQuery } from 'wasp/client/operations'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const TaskView = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <TaskView task={task} key={idx} />
    ))}
    </div>
    )
    }

    Most of this code is regular React, the only exception being the special wasp imports:

    We could have called the Query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the Query changes. Remember that Wasp automatically refreshes Queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/vision.html b/docs/0.13.0/vision.html index 086de757e0..a46ec59008 100644 --- a/docs/0.13.0/vision.html +++ b/docs/0.13.0/vision.html @@ -19,8 +19,8 @@ - - + +
    @@ -33,7 +33,7 @@ They can be used inline (mixed with Wasp code) or provided via external files.
  • Has hatches (escape mechanisms) that allow you to customize your web app in all the right places, but remain hidden until you need them.
  • Entity (data model) is a first-class citizen - defined via custom Wasp syntax and it integrates very closely with the rest of the features, serving as one of the central concepts around which everything is built.
  • Out of the box support for CRUD UI based on the Entities, to get you quickly going, but also customizable to some level.
  • "Smart" operations (queries and actions) that in most cases automatically figure out when to update, and if not it is easy to define custom logic to compensate for that. User worries about client-server gap as little as possible.
  • Support, directly in Wasp, for declaratively defining simple components and operations.
  • Besides Wasp as a programming language, there will also be a visual builder that generates/edits Wasp code, allowing non-developers to participate in development. Since Wasp is declarative, we imagine such builder to naturally follow from Wasp language.
  • Server side rendering, caching, packaging, security, ... -> all those are taken care of by Wasp. You tell Wasp what you want, and Wasp figures out how to do it.
  • As simple deployment to production/staging as it gets.
  • While it comes with the official implementation(s), Wasp language will not be coupled with the single implementation. Others can provide implementations that compile to different web app stacks.
  • - - + + \ No newline at end of file diff --git a/docs/0.13.0/wasp-ai/creating-new-app.html b/docs/0.13.0/wasp-ai/creating-new-app.html index e572480c3f..c3ea89af3e 100644 --- a/docs/0.13.0/wasp-ai/creating-new-app.html +++ b/docs/0.13.0/wasp-ai/creating-new-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.13.0

    Creating New App with AI

    Wasp comes with its own AI: Wasp AI, aka Mage (Magic web App GEnerator).

    Wasp AI allows you to create a new Wasp app from only a title and a short description (using GPT in the background)!

    There are two main ways to create a new Wasp app with Wasp AI:

    1. Free, open-source online app usemage.ai.
    2. Running wasp new on your machine and picking AI generation. For this you need to provide your own OpenAI API keys, but it allows for more flexibility (choosing GPT models).

    They both use the same logic in the background, so both approaches are equally "smart", the difference is just in the UI / settings.

    info

    Wasp AI is an experimental feature. Apps that Wasp AI generates can have mistakes (proportional to their complexity), but even then they can often serve as a great starting point (once you fix the mistakes) or an interesting way to explore how to implement stuff in Wasp.

    usemage.ai

    1. Describe your app 2. Pick the color 3. Generate your app 🚀

    Mage is an open-source app with which you can create new Wasp apps from just a short title and description.

    It is completely free for you - it uses our OpenAI API keys and we take on the costs.

    Once you provide an app title, app description, and choose some basic settings, your new Wasp app will be created for you in a matter of minutes and you will be able to download it to your machine and keep working on it!

    If you want to know more, check this blog post for more details on how Mage works, or this blog post for a high-level overview of how we implemented it.

    Wasp CLI

    You can create a new Wasp app using Wasp AI by running wasp new in your terminal and picking AI generation.

    If you don't have them set yet, wasp will ask you to provide (via ENV vars) your OpenAI API keys (which it will use to query GPT).

    Then, after providing a title and description for your Wasp app, the new app will be generated on your disk!

    wasp-cli-ai-input wasp-cli-ai-generation

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/wasp-ai/developing-existing-app.html b/docs/0.13.0/wasp-ai/developing-existing-app.html index 762d50382c..25a8b38c21 100644 --- a/docs/0.13.0/wasp-ai/developing-existing-app.html +++ b/docs/0.13.0/wasp-ai/developing-existing-app.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.13.0

    Developing Existing App with AI

    While Wasp AI doesn't at the moment offer any additional help for developing your Wasp app with AI beyond initial generation, this is something we are exploring actively.

    In the meantime, while waiting for Wasp AI to add support for this, we suggest checking out aider, which is an AI pair programming tool in your terminal. This is a third-party tool, not affiliated with Wasp in any way, but we and some of Wasp users have found that it can be helpful when working on Wasp apps.

    - - + + \ No newline at end of file diff --git a/docs/0.13.0/writingguide.html b/docs/0.13.0/writingguide.html index 1651940a13..9a76c24f97 100644 --- a/docs/0.13.0/writingguide.html +++ b/docs/0.13.0/writingguide.html @@ -19,8 +19,8 @@ - - + +
    @@ -53,7 +53,7 @@ Many of our titles are currently in title-case, we should start phasing those out.
  • Use the Oxford comma (e.g., "a, b, and c" instead of "a, b and c"). Why the Oxford comma is important
  • Content and Communication

    • Excellence comes from iteration. First drafts are always bad, but writing them is a vital part of the process. It's extremely difficult to avoid the slow progression of Bad -> OK -> Good -> Great -> Inspiring -> Transcendent.
    • Only wait until something is "Good" before publishing. Vue's guide originally says: "The community will help you push it further down the chain." We don't yet have that luxury, as our community isn't large enough. Still, we can't afford to invest too much time into the docs, so "Good" will have to do for now.

    Processes

    • Ideally, you should write the docs before you implement the feature. This will help you see the feature from the user's perspective and better spot the API's deficiencies and improvement potential. If something is difficult to explain, it's most likely difficult to understand. If it is difficult to understand, there might be a better way of designing it.
    • Try not to get defensive when receiving feedback. Our writing can be very personal to us, but if we get upset with the people who help us improve it, they will either stop giving feedback or start limiting the kind of feedback they give.
    • Proofread your work before showing it to others (and use Grammarly). If you show someone work with many spelling/grammar mistakes, you'll get feedback about spelling grammar/mistakes instead of more valuable notes about whether the writing is achieving your goals.
    • When you ask people for feedback, tell reviewers:
      • What you're trying to do.
      • What your fears are.
      • Which balances you're trying to strike.
    • Do your best to come up with a good and straightforward way to say something. Again, this will help the reviewer focus on high-level issues instead of rephrasing your sentences.
    • Read and correct your text several times before submitting it (preferably with some time between the readings). This is similar to proofreading but has more to do with content and communication style than grammar. A time offset is beneficial because it removes the text from your short-term memory, helping you view it more objectively.
    • It's OK to ask AI to improve your text. Just make sure to check it and correct it. You should always sign off on the last version.
    • When someone reports a problem, there is almost always a problem, even if the solution they proposed isn't quite right. Keep asking follow-up questions to learn more.
    • People need to feel safe asking questions when contributing/reviewing content. Here's how you can do that:
      • Thank people for their contributions/reviews, even if you're feeling grumpy. For example:
        • "Great question!"
        • "Thanks for taking the time to explain. 🙂"
        • "This is actually intentional, but thanks for taking the time to contribute. 😊"
      • Listen to what people are saying and mirror if you're not sure you're understanding correctly. This can help validate people's feelings and experiences while also understanding if you're understanding them correctly.
      • Use a lot of positive and empathetic emojis. It's always better to seem a little strange than mean or impatient. This primarily applies to Wasp team members speaking to outside contributors. Since most of the core team knows each other pretty well, there's no need to go overboard with the emojis and pleasantries.
      • Kindly communicate rules/boundaries. If someone behaves in a way that's abusive/inappropriate, respond only with kindness and maturity, but also make it clear that this behavior is not acceptable and what will happen (according to the code of conduct) if they continue behaving poorly.
    • All docs must go through the review cycle, preferably with more than a single reviewer. Different people focus on different things. Some of us are great at coming up with examples, others easily come up with analogies and explain complex topics, some have a clear and concise writing style, etc. Therefore, try to get at least two or three people to review your document.

    Possible improvements

    • Some parts of our docs don't follow all the guidelines outlined in this document. There's no need to start fixing all the issues right away. We can slowly improve the docs as we edit them.
    • We've discussed having a git repo with all the example code in the docs. This should make copying, pasting, testing, and maintaining code snippets easier.
    - - + + \ No newline at end of file diff --git a/docs/advanced/accessing-app-config.html b/docs/advanced/accessing-app-config.html index 24ec8a2a7a..0137e93c8a 100644 --- a/docs/advanced/accessing-app-config.html +++ b/docs/advanced/accessing-app-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -36,7 +36,7 @@ Wasp automatically sets it during development when you run wasp start.
    In production, it should contain the value of your server's URL as the user's browser sees it (i.e., with the DNS and proxies considered).

    You can access it like this:

    import { config } from 'wasp/client'

    console.log(config.apiUrl)
    - - + + \ No newline at end of file diff --git a/docs/advanced/apis.html b/docs/advanced/apis.html index dec25915b3..a4b9e21ca7 100644 --- a/docs/advanced/apis.html +++ b/docs/advanced/apis.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Custom HTTP API Endpoints

    In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

    How to Create an API

    APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

    To create a Wasp API, you must:

    1. Declare the API in Wasp using the api declaration
    2. Define the API's NodeJS implementation

    After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

    Declaring the API in Wasp

    First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

    main.wasp
    // ...

    api fooBar { // APIs and their implementations don't need to (but can) have the same name.
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar")
    }

    Read more about the supported fields in the API Reference.

    Defining the API's NodeJS Implementation

    After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

    1. req: Express Request object
    2. res: Express Response object
    3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
    src/apis.js
    export const fooBar = (req, res, context) => {
    res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
    res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
    };

    Using the API

    Using the API externally

    To use the API externally, you simply call the endpoint using the method and path you used.

    For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

    Using the API from the Client

    To use the API from your client, including with auth support, you can import the Axios wrapper from wasp/client/api and invoke a call. For example:

    src/pages/SomePage.jsx
    import React, { useEffect } from "react";
    import { api } from "wasp/client/api";

    async function fetchCustomRoute() {
    const res = await api.get("/foo/bar");
    console.log(res.data);
    }

    export const Foo = () => {
    useEffect(() => {
    fetchCustomRoute();
    }, []);

    return <>// ...</>;
    };

    Making Sure CORS Works

    APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

    You can do this by defining custom middleware for your APIs in the Wasp file.

    For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

    main.wasp
    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo"
    }

    And then in the implementation file:

    src/apis.js
    export const apiMiddleware = (config) => {
    return config;
    };

    We are returning the default middleware which enables CORS for all APIs under the /foo path.

    For more information about middleware configuration, please see: Middleware Configuration

    Using Entities in APIs

    In many cases, resources used in APIs will be Entities. To use an Entity in your API, add it to the api declaration in Wasp:

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    entities: [Task],
    httpRoute: (GET, "/foo/bar")
    }

    Wasp will inject the specified Entity into the APIs context argument, giving you access to the Entity's Prisma API:

    src/apis.js
    export const fooBar = (req, res, context) => {
    res.json({ count: await context.entities.Task.count() });
    };

    The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

    API Reference

    main.wasp
    api fooBar {
    fn: import { fooBar } from "@src/apis",
    httpRoute: (GET, "/foo/bar"),
    entities: [Task],
    auth: true,
    middlewareConfigFn: import { apiMiddleware } from "@src/apis"
    }

    The api declaration has the following fields:

    • fn: ExtImport required

      The import statement of the APIs NodeJs implementation.

    • httpRoute: (HttpMethod, string) required

      The HTTP (method, path) pair, where the method can be one of:

      • ALL, GET, POST, PUT or DELETE
      • and path is an Express path string.
    • entities: [Entity]

      A list of entities you wish to use inside your API. You can read more about it here.

    • auth: bool

      If auth is enabled, this will default to true and provide a context.user object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to false.

    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function for this API. See more in middleware section of the docs.

    - - + + \ No newline at end of file diff --git a/docs/advanced/deployment/cli.html b/docs/advanced/deployment/cli.html index 96ab11d628..ea83cde8ea 100644 --- a/docs/advanced/deployment/cli.html +++ b/docs/advanced/deployment/cli.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ The command automates the manual deployment process and is the recommended way of deploying Wasp apps.

    Supported Providers

    Wasp supports automated deployment to the following providers:

    • Fly.io - they offer 5$ free credit each month
    • Railway (coming soon, track it here #1157)

    Fly.io

    Prerequisites

    Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.

    You can add the required credit card information on the account's billing page.

    Fly.io CLI

    You will need the flyctl CLI installed on your machine before you can deploy to Fly.io.

    Deploying

    Using the Wasp CLI, you can easily deploy a new app to Fly.io with just a single command:

    wasp deploy fly launch my-wasp-app mia
    Specifying Org

    If your account is a member of more than one organization on Fly.io, you will need to specify under which one you want to execute the command. To do that, provide an additional --org <org-slug> option. You can find out the names(slugs) of your organizations by running fly orgs list.

    Please do not CTRL-C or exit your terminal while the commands are running.

    Under the covers, this runs the equivalent of the following commands:

    wasp deploy fly setup my-wasp-app mia
    wasp deploy fly create-db mia
    wasp deploy fly deploy

    The commands above use the app basename my-wasp-app and deploy it to the Miami, Florida (US) region (called mia). Read more about Fly.io regions here.

    Unique Name

    Your app name must be unique across all of Fly or deployment will fail.

    The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:

    • my-wasp-app-client
    • my-wasp-app-server
    • my-wasp-app-db

    You'll notice that Wasp creates two new files in your project root directory:

    • fly-server.toml
    • fly-client.toml

    You should include these files in your version control so that you can deploy your app with a single command in the future.

    Using a Custom Domain For Your App

    Setting up a custom domain is a three-step process:

    1. You need to add your domain to your Fly client app. You can do this by running:
    wasp deploy fly cmd --context client certs create mycoolapp.com
    Use Your Domain

    Make sure to replace mycoolapp.com with your domain in all of the commands mentioned in this section.

    This command will output the instructions to add the DNS records to your domain. It will look something like this:

    You can direct traffic to mycoolapp.com by:

    1: Adding an A record to your DNS service which reads

    A @ 66.241.1XX.154

    You can validate your ownership of mycoolapp.com by:

    2: Adding an AAAA record to your DNS service which reads:

    AAAA @ 2a09:82XX:1::1:ff40
    1. You need to add the DNS records for your domain:

      This will depend on your domain provider, but it should be a matter of adding an A record for @ and an AAAA record for @ with the values provided by the previous command.

    2. You need to set your domain as the WASP_WEB_CLIENT_URL environment variable for your server app:

    wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com

    We need to do this to keep our CORS configuration up to date.

    That's it, your app should be available at https://mycoolapp.com! 🎉

    Adding www Subdomain

    If you'd like to also access your app at https://www.mycoolapp.com, you can generate certs for the www subdomain.

    wasp deploy fly cmd --context client certs create www.mycoolapp.com

    Once you do that, you will need to add another DNS record for your domain. It should be a CNAME record for www with the value of your root domain. Here's an example:

    TypeNameValueTTL
    CNAMEwwwmycoolapp.com3600

    With the CNAME record (Canonical name), you are assigning the www subdomain as an alias to the root domain.

    Your app should now be available both at the root domain https://mycoolapp.com and the www sub-domain https://www.mycoolapp.com! 🎉

    API Reference

    launch

    launch is a convenience command that runs setup, create-db, and deploy in sequence.

    wasp deploy fly launch <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    It gives you the same result as running the following commands:

    wasp deploy fly setup <app-name> <region>
    wasp deploy fly create-db <region>
    wasp deploy fly deploy

    Environment Variables

    Server

    If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the --server-secret option:

    wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>
    Client

    If you've added any client-side environment variables to your app, make sure to pass them to the terminal session before running the launch command, e.g.:

    REACT_APP_ANOTHER_VAR=somevalue wasp deploy fly launch my-wasp-app mia

    setup

    setup will create your client and server apps on Fly, and add some secrets, but does not deploy them.

    wasp deploy fly setup <app-name> <region>

    It accepts the following arguments:

    • <app-name> - the name of your app required

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    After running setup, Wasp creates two new files in your project root directory: fly-server.toml and fly-client.toml. You should include these files in your version control.

    You can edit the fly-server.toml and fly-client.toml files to further configure your Fly deployments. Wasp will use the TOML files when you run deploy.

    If you want to maintain multiple apps, you can add the --fly-toml-dir <abs-path> option to point to different directories, like "dev" or "staging".

    Execute Only Once

    You should only run setup once per app. If you run it multiple times, it will create unnecessary apps on Fly.

    create-db

    create-db will create a new database for your app.

    wasp deploy fly create-db <region>

    It accepts the following arguments:

    • <region> - the region where your app will be deployed required

      Read how to find the available regions here.

    Execute Only Once

    You should only run create-db once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.

    deploy

    wasp deploy fly deploy

    deploy pushes your client and server live.

    Run this command whenever you want to update your deployed app with the latest changes:

    wasp deploy fly deploy

    If you've added any client-side environment variables to your app, make sure to pass them to the terminal session before running the deploy command, e.g.:

    REACT_APP_ANOTHER_VAR=somevalue wasp deploy fly deploy

    Make sure to add your client-side environment variables every time you redeploy with the above command to ensure they are included in the build process!

    cmd

    If want to run arbitrary Fly commands (e.g. flyctl secrets list for your server app), here's how to do it:

    wasp deploy fly cmd secrets list --context server

    Environment Variables

    Server Secrets

    If your app requires any other server-side environment variables (like social auth secrets), you can set them:

    1. initially in the launch command with the --server-secret option,
      or
    2. after the app has already been deployed by using the secrets set command:
    wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server

    Client Environment Variables

    If you've added any client-side environment variables to your app, make sure to pass them to the terminal session before running a deployment command, e.g.:

    REACT_APP_ANOTHER_VAR=somevalue wasp deploy fly launch my-wasp-app mia

    or

    REACT_APP_ANOTHER_VAR=somevalue wasp deploy fly deploy

    Fly.io Regions

    Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.

    Read more on Fly regions here.

    You can find the list of all available Fly regions by running:

    flyctl platform regions

    Multiple Fly.io Organizations

    If you have multiple organizations, you can specify a --org option. For example:

    wasp deploy fly launch my-wasp-app mia --org hive

    Building Locally

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.

    If you want to build locally, supply the --build-locally option to wasp deploy fly launch or wasp deploy fly deploy.

    - - + + \ No newline at end of file diff --git a/docs/advanced/deployment/manually.html b/docs/advanced/deployment/manually.html index 6b6f9a11cf..0ade1feaeb 100644 --- a/docs/advanced/deployment/manually.html +++ b/docs/advanced/deployment/manually.html @@ -19,8 +19,8 @@ - - + +
    @@ -40,7 +40,7 @@ Set it to a random string at least 32 characters long (you can use an online generator).

  • PORT

    The server's HTTP port number. This is where the server listens for requests (default: 3001).

  • Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    While these are the general instructions on deploying the server anywhere, we also have more detailed instructions for chosen providers below, so check that out for more guidance if you are deploying to one of those providers.

    3. Deploying the Web Client (frontend)

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    Client Environment Variables

    Remember, if you have manually defined any other client-side environment variables in your project, make sure to add them to the command above when building your client

    The command above will build the web client and put it in the build/ directory in the .wasp/build/web-app/.

    This is also the moment to provide any additional env vars for the client code, next to REACT_APP_API_URL. Check the env vars docs for more details.

    Since the result of building is just a bunch of static files, you can now deploy your web client to any static hosting provider (e.g. Netlify, Cloudflare, ...) by deploying the contents of .wasp/build/web-app/build/.

    4. Deploying the Database

    Any PostgreSQL database will do, as long as you provide the server with the correct DATABASE_URL env var and ensure that the database is accessible from the server.

    Different Providers

    We'll cover a few different deployment providers below:

    • Fly.io (server and database)
    • Netlify (client)
    • Railway (server, client and database)
    • Heroku (server and database)

    Fly.io (server and database)

    We will show how to deploy the server and provision a database for it on Fly.io.

    We automated this process for you

    If you want to do all of the work below with one command, you can use the Wasp CLI.

    Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command.

    Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the flyctl CLI.

    note

    Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.

    Additionally, fly is a symlink for flyctl on most systems and they can be used interchangeably.

    Make sure you are logged in with flyctl CLI. You can check if you are logged in with flyctl auth whoami, and if you are not, you can log in with flyctl auth login.

    Set Up a Fly.io App

    info

    You need to do this only once per Wasp app.

    Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    Next, run the launch command to set up a new app and create a fly.toml file:

    flyctl launch --remote-only

    This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.

    • Say yes to Would you like to set up a PostgreSQL database now? and select Development. Fly.io will set a DATABASE_URL for you.

    • Say no to Would you like to deploy now? (and to any additional questions).

      We still need to set up several environment variables.

    What if the database setup fails?

    If your attempts to initiate a new app fail for whatever reason, then you should run flyctl apps destroy <app-name> before trying again. Fly does not allow you to create multiple apps with the same name.

    What does it look like when your DB is deployed correctly?

    When your DB is deployed correctly, you'll see it in the Fly.io dashboard:

    image

    Next, let's copy the fly.toml file up to our Wasp project dir for safekeeping.

    cp fly.toml ../../

    Next, add a few more environment variables for the server code.

    flyctl secrets set PORT=8080
    flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
    flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_client_will_be_deployed>
    flyctl secrets set WASP_SERVER_URL=<url_of_where_server_will_be_deployed>
    note

    If you do not know what your client URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your client.

    Using an external auth method?

    If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    If you want to make sure you've added your secrets correctly, run flyctl secrets list in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.

    Deploy to a Fly.io App

    While still in the .wasp/build/ directory, run:

    flyctl deploy --remote-only --config ../../fly.toml

    This will build and deploy the backend of your Wasp app on Fly.io to https://<app-name>.fly.dev 🤘🎸

    Now, if you haven't, you can deploy your client and add the client URL by running flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_client>. We suggest using Netlify for your client, but you can use any static hosting provider.

    Additionally, some useful flyctl commands:

    flyctl logs
    flyctl secrets list
    flyctl ssh console

    Redeploying After Wasp Builds

    When you rebuild your Wasp app (with wasp build), it will remove your .wasp/build/ directory. In there, you may have a fly.toml from any prior Fly.io deployments.

    While we will improve this process in the future, in the meantime, you have a few options:

    1. Copy the fly.toml file to a versioned directory, like your Wasp project dir.

      From there, you can reference it in flyctl deploy --config <path> commands, like above.

    2. Backup the fly.toml file somewhere before running wasp build, and copy it into .wasp/build/ after.

      When the fly.toml file exists in .wasp/build/ dir, you do not need to specify the --config <path>.

    3. Run flyctl config save -a <app-name> to regenerate the fly.toml file from the remote state stored in Fly.io.

    Netlify (client)

    We'll show how to deploy the client on Netlify.

    Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and Netlify CLI installed to follow these instructions.

    Make sure you are logged in with Netlify CLI. You can check if you are logged in with netlify status, and if you are not, you can log in with netlify login.

    First, make sure you have built the Wasp app. We'll build the client web app next.

    To build the web app, position yourself in .wasp/build/web-app directory:

    cd .wasp/build/web-app

    Run

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

    where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

    Client Environment Variables

    Remember, if you have manually defined any other client-side environment variables in your project, make sure to add them to the command above when building your client

    We can now deploy the client with:

    netlify deploy

    Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.

    The final step is to run:

    netlify deploy --prod

    That is it! Your client should be live at https://<app-name>.netlify.app

    note

    Make sure you set this URL as the WASP_WEB_CLIENT_URL environment variable in your server hosting environment (e.g., Fly.io or Heroku).

    Railway (server, client and database)

    We will show how to deploy the client, the server, and provision a database on Railway.

    Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their plans page for more info.

    Prerequisites

    To get started, follow these steps:

    1. Make sure your Wasp app is built by running wasp build in the project dir.

    2. Create a Railway account

      Free Tier

      Sign up with your GitHub account to be eligible for the free tier

    3. Install the Railway CLI

    4. Run railway login and a browser tab will open to authenticate you.

    Create New Project

    Let's create our Railway project:

    1. Go to your Railway dashboard, click on New Project, and select Provision PostgreSQL from the dropdown menu.
    2. Once it initializes, right-click on the New button in the top right corner and select Empty Service.
    3. Once it initializes, click on it, go to Settings > General and change the name to server
    4. Go ahead and create another empty service and name it client

    Changing the name

    Deploy Your App to Railway

    Setup Domains

    We'll need the domains for both the server and client services:

    1. Go to the server instance's Settings tab, and click Generate Domain.
    2. Do the same under the client's Settings.

    Copy the domains as we will need them later.

    Deploying the Server

    Let's deploy our server first:

    1. Move into your app's .wasp/build/ directory:

      cd .wasp/build
    2. Link your app build to your newly created Railway project:

      railway link
    3. Go into the Railway dashboard and set up the required env variables:

      Open the Settings and go to the Variables tab:

      • click Variable reference and select DATABASE_URL (it will populate it with the correct value)

      • add WASP_WEB_CLIENT_URL - enter the client domain (e.g. https://client-production-XXXX.up.railway.app). https:// prefix is required!

      • add WASP_SERVER_URL - enter the server domain (e.g. https://server-production-XXXX.up.railway.app). https:// prefix is required!

      • add JWT_SECRET - enter a random string at least 32 characters long (use an online generator)

        Using an external auth method?

        If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to additionally set the necessary environment variables specifically required by these method(s).

    4. Push and deploy the project:

    railway up

    Select server when prompted with Select Service.

    Railway will now locate the Dockerfile and deploy your server 👍

    Deploying the Client

    1. Next, change into your app's frontend build directory .wasp/build/web-app:

      cd web-app
    2. Create the production build, using the server domain as the REACT_APP_API_URL:

      npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
    3. Next, we want to link this specific frontend directory to our project as well:

      railway link
    4. We need to configure Railway's static hosting for our client.

      Setting Up Static Hosting

      Copy the build folder within the web-app directory to dist:

      cp -r build dist

      We'll need to create the following files:

      • Dockerfile with:

        Dockerfile
        FROM pierrezemb/gostatic
        CMD [ "-fallback", "index.html" ]
        COPY ./dist/ /srv/http/
      • .dockerignore with:

        .dockerignore
        node_modules/

      You'll need to repeat these steps each time you run wasp build as it will remove the .wasp/build/web-app directory.

      Here's a useful shell script to do the process

      If you want to automate the process, save the following as deploy_client.sh in the root of your project:

      deploy_client.sh
      #!/usr/bin/env bash

      if [ -z "$REACT_APP_API_URL" ]
      then
      echo "REACT_APP_API_URL is not set"
      exit 1
      fi

      wasp build
      cd .wasp/build/web-app

      npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build

      cp -r build dist

      dockerfile_contents=$(cat <<EOF
      FROM pierrezemb/gostatic
      CMD [ "-fallback", "index.html" ]
      COPY ./dist/ /srv/http/
      EOF
      )

      dockerignore_contents=$(cat <<EOF
      node_modules/
      EOF
      )

      echo "$dockerfile_contents" > Dockerfile
      echo "$dockerignore_contents" > .dockerignore

      railway up

      Make it executable with:

      chmod +x deploy_client.sh

      You can run it with:

      REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
    5. Set the PORT environment variable to 8043 under the Variables tab.

    6. Once set, deploy the client and select client when prompted with Select Service:

    railway up

    Conclusion

    And now your Wasp should be deployed! 🐝 🚂 🚀

    Back in your Railway dashboard, click on your project and you should see your newly deployed services: PostgreSQL, Server, and Client.

    Updates & Redeploying

    When you make updates and need to redeploy:

    • run wasp build to rebuild your app
    • run railway up in the .wasp/build directory (server)
    • repeat all the steps in the .wasp/build/web-app directory (client)

    Heroku (server and database)

    We will show how to deploy the server and provision a database for it on Heroku.

    note

    Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

    As such, we recommend using an alternative provider like Fly.io for your first apps.

    You will need Heroku account, heroku CLI and docker CLI installed to follow these instructions.

    Make sure you are logged in with heroku CLI. You can check if you are logged in with heroku whoami, and if you are not, you can log in with heroku login.

    Set Up a Heroku App

    info

    You need to do this only once per Wasp app.

    Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:

    heroku create <app-name>

    Unless you have an external PostgreSQL database that you want to use, let's create a new database on Heroku and attach it to our app:

    heroku addons:create --app <app-name> heroku-postgresql:mini
    caution

    Heroku does not offer a free plan anymore and mini is their cheapest database instance - it costs $5/mo.

    Heroku will also set DATABASE_URL env var for us at this point. If you are using an external database, you will have to set it up yourself.

    The PORT env var will also be provided by Heroku, so the ones left to set are the JWT_SECRET, WASP_WEB_CLIENT_URL and WASP_SERVER_URL env vars:

    heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
    heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_client_will_be_deployed>
    heroku config:set --app <app-name> WASP_SERVER_URL=<url_of_where_server_will_be_deployed>
    note

    If you do not know what your client URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your client.

    Deploy to a Heroku App

    After you have built the app, position yourself in .wasp/build/ directory:

    cd .wasp/build

    assuming you were at the root of your Wasp project at that moment.

    Log in to Heroku Container Registry:

    heroku container:login

    Build the docker image and push it to Heroku:

    heroku container:push --app <app-name> web

    App is still not deployed at this point. This step might take some time, especially the very first time, since there are no cached docker layers.

    Note for Apple Silicon Users

    Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of heroku container:push, users instead should:

    docker buildx build --platform linux/amd64 -t <app-name> .
    docker tag <app-name> registry.heroku.com/<app-name>/web
    docker push registry.heroku.com/<app-name>/web

    You are now ready to proceed to the next step.

    Deploy the pushed image and restart the app:

    heroku container:release --app <app-name> web

    This is it, the backend is deployed at https://<app-name>-XXXX.herokuapp.com 🎉

    Find out the exact app URL with:

    heroku info --app <app-name>

    Additionally, you can check out the logs with:

    heroku logs --tail --app <app-name>
    Using pg-boss with Heroku

    If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.

    Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js

    Koyeb (server, client and database)

    Check out the tutorial made by the team at Koyeb for detailed instructions on how to deploy a whole Wasp app on Koyeb: Using Wasp to Build Full-Stack Web Applications on Koyeb.

    The tutorial was written for Wasp v0.13.

    - - + + \ No newline at end of file diff --git a/docs/advanced/deployment/overview.html b/docs/advanced/deployment/overview.html index 7e8f9c0e5a..589d7ade82 100644 --- a/docs/advanced/deployment/overview.html +++ b/docs/advanced/deployment/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It also runs any pending migrations.

    You can add extra steps to this multi-stage Dockerfile by creating your own Dockerfile in the project's root directory. If Wasp finds a Dockerfile in the project's root, it appends its contents at the bottom of the default multi-stage Dockerfile.

    Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

    A few things to keep in mind:

    • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
    • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
    • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

    Read more in the official Docker docs on multi-stage builds.

    To see what your project's (potentially combined) Dockerfile will look like, run:

    wasp dockerfile

    Join our Discord if you have any questions, or if you need more customization than this hook provides.

    - - + + \ No newline at end of file diff --git a/docs/advanced/email.html b/docs/advanced/email.html index be8d8d68fd..de047360e3 100644 --- a/docs/advanced/email.html +++ b/docs/advanced/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Sending Emails

    With Wasp's email-sending feature, you can easily integrate email functionality into your web application.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    Choose from one of the providers:

    • Dummy (development only),
    • Mailgun,
    • SendGrid
    • or the good old SMTP.

    Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

    Sending Emails

    Before jumping into details about setting up various providers, let's see how easy it is to send emails.

    You import the emailSender that is provided by the wasp/server/email module and call the send method on it.

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    Read more about the send method in the API Reference.

    The send method returns an object with the status of the sent email. It varies depending on the provider you use.

    Providers

    We'll go over all of the available providers in the next section. For some of them, you'll need to set up some env variables. You can do that in the .env.server file.

    Using the Dummy Provider

    Dummy Provider is not for production use

    The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    To speed up development, Wasp offers a Dummy email sender that console.logs the emails in the console. Since it doesn't send emails for real, it doesn't require any setup.

    Set the provider to Dummy in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Dummy,
    }
    }

    Using the SMTP Provider

    First, set the provider to SMTP in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SMTP,
    }
    }

    Then, add the following env variables to your .env.server file.

    .env.server
    SMTP_HOST=
    SMTP_USERNAME=
    SMTP_PASSWORD=
    SMTP_PORT=

    Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

    Using the Mailgun Provider

    Set the provider to Mailgun in the main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: Mailgun,
    }
    }

    Then, get the Mailgun API key and domain and add them to your .env.server file.

    Getting the API Key and Domain

    1. Go to Mailgun and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    4. Go to Domains and create a new domain.
    5. Copy the domain and add it to your .env.server file.
    .env.server
    MAILGUN_API_KEY=
    MAILGUN_DOMAIN=

    Using the SendGrid Provider

    Set the provider field to SendGrid in your main.wasp file.

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: SendGrid,
    }
    }

    Then, get the SendGrid API key and add it to your .env.server file.

    Getting the API Key

    1. Go to SendGrid and create an account.
    2. Go to API Keys and create a new API key.
    3. Copy the API key and add it to your .env.server file.
    .env.server
    SENDGRID_API_KEY=

    API Reference

    emailSender dict

    main.wasp
    app Example {
    ...
    emailSender: {
    provider: <provider>,
    defaultFrom: {
    name: "Example",
    email: "hello@itsme.com"
    },
    }
    }

    The emailSender dict has the following fields:

    • provider: Provider required

      The provider you want to use. Choose from Dummy, SMTP, Mailgun or SendGrid.

      Dummy Provider is not for production use

      The Dummy provider is not for production use. It is only meant to be used during development. If you try building your app with the Dummy provider, the build will fail.

    • defaultFrom: dict

      The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

    JavaScript API

    Using the emailSender in :

    src/actions/sendEmail.js
    import { emailSender } from "wasp/server/email";

    // In some action handler...
    const info = await emailSender.send({
    from: {
    name: "John Doe",
    email: "john@doe.com",
    },
    to: "user@domain.com",
    subject: "Saying hello",
    text: "Hello world",
    html: "Hello <strong>world</strong>",
    });

    The send method accepts an object with the following fields:

    • from: object

      The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

      • name: string

        The name of the sender.

      • email: string

        The email address of the sender.

    • to: string required

      The recipient's email address.

    • subject: string required

      The subject of the email.

    • text: string required

      The text version of the email.

    • html: string required

      The HTML version of the email

    - - + + \ No newline at end of file diff --git a/docs/advanced/jobs.html b/docs/advanced/jobs.html index a800938c3f..6451ffb953 100644 --- a/docs/advanced/jobs.html +++ b/docs/advanced/jobs.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Recurring Jobs

    In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

    What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

    Wasp supports background jobs that can help you with this:

    • Jobs persist between server restarts,
    • Jobs can be retried if they fail,
    • Jobs can be delayed until a future time,
    • Jobs can have a recurring schedule.

    Using Jobs

    Job Definition and Usage

    Let's write an example Job that will print a message to the console and return a list of tasks from the database.

    1. Start by creating a Job declaration in your .wasp file:

      main.wasp
      job mySpecialJob {
      executor: PgBoss,
      perform: {
      fn: import { foo } from "@src/workers/bar"
      },
      entities: [Task],
      }
    2. After declaring the Job, implement its worker function:

      src/workers/bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
      The worker function

      The worker function must be an async function. The function's return value represents the Job's result.

      The worker function accepts two arguments:

      • args: The data passed into the job when it's submitted.
      • context: { entities }: The context object containing entities you put in the Job declaration.
    3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'

      const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

      // Or, if you'd prefer it to execute in the future, just add a .delay().
      // It takes a number of seconds, Date, or ISO date string.
      await mySpecialJob
      .delay(10)
      .submit({ name: "Johnny" })

    And that's it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

    In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

    Recurring Jobs

    If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar"
    },
    schedule: {
    cron: "0 * * * *",
    args: {=json { "job": "args" } json=} // optional
    }
    }

    In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

    API Reference

    Declaring Jobs

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar",
    executorOptions: {
    pgBoss: {=json { "retryLimit": 1 } json=}
    }
    },
    schedule: {
    cron: "*/5 * * * *",
    args: {=json { "foo": "bar" } json=},
    executorOptions: {
    pgBoss: {=json { "retryLimit": 0 } json=}
    }
    },
    entities: [Task],
    }

    The Job declaration has the following fields:

    • executor: JobExecutor required

      Job executors

      Our jobs need job executors to handle the scheduling, monitoring, and execution.

      PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires that your database provider is set to "postgresql" in your schema.prisma file. Read more about setting the provider here.

      We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

      info

      Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

      pg-boss details

      pg-boss provides many useful features, which can be found here.

      When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

      If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

      pg-boss considerations

      • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
        • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
      • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
        • If you remove a schedule from a job, you will need to do the above as well.
      • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
      • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
    • perform: dict required

      • fn: ExtImport required

        • An async function that performs the work. Since Wasp executes Jobs on the server, the import path must lead to a NodeJS file.
        • It receives the following arguments:
          • args: Input: The data passed to the job when it's submitted.
          • context: { entities: Entities }: The context object containing any declared entities.

        Here's an example of a perform.fn function:

        src/workers/bar.js
        export const foo = async ({ name }, context) => {
        console.log(`Hello ${name}!`)
        const tasks = await context.entities.Task.findMany({})
        return { tasks }
        }
      • executorOptions: dict

        Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

        • pgBoss: JSON

          See the docs for pg-boss.

    • schedule: dict

      • cron: string required

        A 5-placeholder format cron expression string. See rationale for minute-level precision here.

        If you need help building cron expressions, Check out Crontab guru.

      • args: JSON

        The arguments to pass to the perform.fn function when invoked.

      • executorOptions: dict

        Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

        • pgBoss: JSON

          See the docs for pg-boss.

    • entities: [Entity]

      A list of entities you wish to use inside your Job (similar to Queries and Actions).

    JavaScript API

    • Importing a Job:

      someAction.js
      import { mySpecialJob } from 'wasp/server/jobs'
    • submit(jobArgs, executorOptions)

      • jobArgs: Input

      • executorOptions: object

        Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

      someAction.js
      const submittedJob = await mySpecialJob.submit({ job: "args" })
    • delay(startAfter)

      • startAfter: int | string | Date required

        Delaying the invocation of the job handler. The delay can be one of:

        • Integer: number of seconds to delay. [Default 0]
        • String: ISO date string to run at.
        • Date: Date to run at.
      someAction.js
      const submittedJob = await mySpecialJob
      .delay(10)
      .submit({ job: "args" }, { "retryLimit": 2 })

    Tracking

    The return value of submit() is an instance of SubmittedJob, which has the following fields:

    • jobId: The ID for the job in that executor.
    • jobName: The name of the job you used in your .wasp file.
    • executorName: The Symbol of the name of the job executor.

    There are also some namespaced, job executor-specific objects.

    • For pg-boss, you may access: pgBoss
      • details(): pg-boss specific job detail information. Reference
      • cancel(): attempts to cancel a job. Reference
      • resume(): attempts to resume a canceled job. Reference
    - - + + \ No newline at end of file diff --git a/docs/advanced/links.html b/docs/advanced/links.html index b6fc37c7dd..6927e53fde 100644 --- a/docs/advanced/links.html +++ b/docs/advanced/links.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Type-Safe Links

    If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

    After you defined a route:

    main.wasp
    route TaskRoute { path: "/task/:id", to: TaskPage }
    page TaskPage { ... }

    You can get the benefits of type-safe links by using the Link component from wasp/client/router:

    TaskList.tsx
    import { Link } from 'wasp/client/router'

    export const TaskList = () => {
    // ...

    return (
    <div>
    {tasks.map((task) => (
    <Link
    key={task.id}
    to="/task/:id"
    {/* 👆 You must provide a valid path here */}
    params={{ id: task.id }}>
    {/* 👆 All the params must be correctly passed in */}
    {task.description}
    </Link>
    ))}
    </div>
    )
    }

    Using Search Query & Hash

    You can also pass search and hash props to the Link component:

    TaskList.tsx
    <Link
    to="/task/:id"
    params={{ id: task.id }}
    search={{ sortBy: 'date' }}
    hash="comments"
    >
    {task.description}
    </Link>

    This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

    The routes Object

    You can also get all the pages in your app with the routes object:

    TaskList.tsx
    import { routes } from 'wasp/client/router'

    const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

    This will result in a link like this: /task/1.

    You can also pass search and hash props to the build function. Check out the API Reference for more details.

    API Reference

    The Link component accepts the following props:

    • to required

      • A valid Wasp Route path from your main.wasp file.
    • params: { [name: string]: string | number } required (if the path contains params)

      • An object with keys and values for each param in the path.
      • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
    • search: string[][] | Record<string, string> | string | URLSearchParams

      • Any valid input for URLSearchParams constructor.
      • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
    • hash: string

    • all other props that the react-router-dom's Link component accepts

    routes Object

    The routes object contains a function for each route in your app.

    router.tsx
    export const routes = {
    // RootRoute has a path like "/"
    RootRoute: {
    build: (options?: {
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }) => // ...
    },

    // DetailRoute has a path like "/task/:id/:something?"
    DetailRoute: {
    build: (
    options: {
    params: { id: ParamValue; something?: ParamValue; },
    search?: string[][] | Record<string, string> | string | URLSearchParams
    hash?: string
    }
    ) => // ...
    }
    }

    The params object is required if the route contains params. The search and hash parameters are optional.

    You can use the routes object like this:

    import { routes } from 'wasp/client/router'

    const linkToRoot = routes.RootRoute.build()
    const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
    - - + + \ No newline at end of file diff --git a/docs/advanced/middleware-config.html b/docs/advanced/middleware-config.html index 38c39bb6e1..45e4b6afe9 100644 --- a/docs/advanced/middleware-config.html +++ b/docs/advanced/middleware-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Configuring Middleware

    Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

    Default Global Middleware 🌍

    Wasp's Express server has the following middleware by default:

    • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

    • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

      note

      CORS middleware is required for the frontend to communicate with the backend.

    • Morgan: HTTP request logger middleware.

    • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

      note

      JSON middleware is required for Operations to function properly.

    • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

    • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

    Customization

    You have three places where you can customize middleware:

    1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

      Modifying global middleware

      Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

    2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

    3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

      • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

    Default Middleware Definitions

    Below is the actual definitions of default middleware which you can override.

    const defaultGlobalMiddleware = new Map([
    ['helmet', helmet()],
    ['cors', cors({ origin: config.allowedCORSOrigins })],
    ['logger', logger('dev')],
    ['express.json', express.json()],
    ['express.urlencoded', express.urlencoded({ extended: false })],
    ['cookieParser', cookieParser()]
    ])

    1. Customize Global Middleware

    If you would like to modify the middleware for all operations and APIs, you can do something like:

    main.wasp
    app todoApp {
    // ...

    server: {
    setupFn: import setup from "@src/serverSetup",
    middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup"
    },
    }
    src/serverSetup.js
    import cors from 'cors'
    import { config } from 'wasp/server'

    export const serverMiddlewareFn = (middlewareConfig) => {
    // Example of adding extra domains to CORS.
    middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
    return middlewareConfig
    }

    2. Customize api-specific Middleware

    If you would like to modify the middleware for a single API, you can do something like:

    main.wasp
    // ...

    api webhookCallback {
    fn: import { webhookCallback } from "@src/apis",
    middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis",
    httpRoute: (POST, "/webhook/callback"),
    auth: false
    }
    src/apis.js
    import express from 'express'

    export const webhookCallback = (req, res, _context) => {
    res.json({ msg: req.body.length })
    }

    export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
    console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

    middlewareConfig.delete('express.json')
    middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

    return middlewareConfig
    }

    note

    This gets installed on a per-method basis. Behind the scenes, this results in code like:

    router.post('/webhook/callback', webhookCallbackMiddleware, ...)

    3. Customize Per-Path Middleware

    If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

    main.wasp
    // ...

    apiNamespace fooBar {
    middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis",
    path: "/foo/bar"
    }
    src/apis.js
    export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
    const customMiddleware = (_req, _res, next) => {
    console.log('fooBarNamespaceMiddlewareFn: custom middleware')
    next()
    }

    middlewareConfig.set('custom.middleware', customMiddleware)

    return middlewareConfig
    }
    note

    This gets installed at the router level for the path. Behind the scenes, this results in something like:

    router.use('/foo/bar', fooBarNamespaceMiddleware)
    - - + + \ No newline at end of file diff --git a/docs/advanced/web-sockets.html b/docs/advanced/web-sockets.html index 48845f2838..ccc09f7786 100644 --- a/docs/advanced/web-sockets.html +++ b/docs/advanced/web-sockets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Web Sockets

    Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

    We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

    To get started, you need to:

    1. Define your WebSocket logic on the server.
    2. Enable WebSockets in your Wasp file, and connect it with your server logic.
    3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
    4. Optionally, type the WebSocket events and payloads for full-stack type safety.

    Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

    Turn On WebSockets in Your Wasp File

    We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    Defining the Events Handler

    Let's define the WebSockets server with all of the events and handler functions.

    webSocketFn Function

    On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

    You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

    This is how we can define our webSocketFn function:

    src/webSocket.js
    import { v4 as uuidv4 } from 'uuid'
    import { getFirstProviderUserId } from 'wasp/auth'

    export const webSocketFn = (io, context) => {
    io.on('connection', (socket) => {
    const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
    console.log('a user connected: ', username)

    socket.on('chatMessage', async (msg) => {
    console.log('message: ', msg)
    io.emit('chatMessage', { id: uuidv4(), username, text: msg })
    // You can also use your entities here:
    // await context.entities.SomeEntity.create({ someField: msg })
    })
    })
    }

    Using the WebSocket On The Client

    The useSocket Hook

    Client access to WebSockets is provided by the useSocket hook. It returns:

    • socket: Socket for sending and receiving events.
    • isConnected: boolean for showing a display of the Socket.IO connection status.
      • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
      • If you set autoConnect: false in your Wasp file, then you should call these as needed.

    All components using useSocket share the same underlying socket.

    The useSocketListener Hook

    Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

    src/ChatPage.jsx
    import React, { useState } from 'react'
    import {
    useSocket,
    useSocketListener,
    } from 'wasp/client/webSocket'

    export const ChatPage = () => {
    const [messageText, setMessageText] = useState('')
    const [messages, setMessages] = useState([])
    const { socket, isConnected } = useSocket()

    useSocketListener('chatMessage', logMessage)

    function logMessage(msg) {
    setMessages((priorMessages) => [msg, ...priorMessages])
    }

    function handleSubmit(e) {
    e.preventDefault()
    socket.emit('chatMessage', messageText)
    setMessageText('')
    }

    const messageList = messages.map((msg) => (
    <li key={msg.id}>
    <em>{msg.username}</em>: {msg.text}
    </li>
    ))
    const connectionIcon = isConnected ? '🟢' : '🔴'

    return (
    <>
    <h2>Chat {connectionIcon}</h2>
    <div>
    <form onSubmit={handleSubmit}>
    <div>
    <div>
    <input
    type="text"
    value={messageText}
    onChange={(e) => setMessageText(e.target.value)}
    />
    </div>
    <div>
    <button type="submit">Submit</button>
    </div>
    </div>
    </form>
    <ul>{messageList}</ul>
    </div>
    </>
    )
    }

    API Reference

    todoApp.wasp
    app todoApp {
    // ...

    webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
    },
    }

    The webSocket dict has the following fields:

    • fn: WebSocketFn required

      The function that defines the WebSocket events and handlers.

    • autoConnect: bool

      Whether to automatically connect to the WebSocket server. Default: true.

    - - + + \ No newline at end of file diff --git a/docs/auth/auth-hooks.html b/docs/auth/auth-hooks.html index 8969adac0c..9e3fbe7f90 100644 --- a/docs/auth/auth-hooks.html +++ b/docs/auth/auth-hooks.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Auth Hooks

    Auth hooks allow you to "hook into" the auth process at various stages and run your custom code. For example, if you want to forbid certain emails from signing up, or if you wish to send a welcome email to the user after they sign up, auth hooks are the way to go.

    Supported hooks

    The following auth hooks are available in Wasp:

    We'll go through each of these hooks in detail. But first, let's see how the hooks fit into the signup flow:

    Signup Flow with Hooks
    Signup Flow with Hooks

    If you are using OAuth, the flow includes extra steps before the signup flow:

    OAuth Flow with Hooks
    OAuth Flow with Hooks

    Using hooks

    To use auth hooks, you must first declare them in the Wasp file:

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    auth: {
    userEntity: User,
    methods: {
    ...
    },
    onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
    onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
    onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
    },
    }

    If the hooks are defined as async functions, Wasp awaits them. This means the auth process waits for the hooks to finish before continuing.

    Wasp ignores the hooks' return values. The only exception is the onBeforeOAuthRedirect hook, whose return value affects the OAuth redirect URL.

    We'll now go through each of the available hooks.

    Executing code before the user signs up

    Wasp calls the onBeforeSignup hook before the user is created.

    The onBeforeSignup hook can be useful if you want to reject a user based on some criteria before they sign up.

    Works with Email Username & Password Discord Github Google Keycloak

    main.wasp
    app myApp {
    ...
    auth: {
    ...
    onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
    },
    }
    src/auth/hooks.js
    import { HttpError } from 'wasp/server'

    export const onBeforeSignup = async ({
    providerId,
    prisma,
    req,
    }) => {
    const count = await prisma.user.count()
    console.log('number of users before', count)
    console.log('provider name', providerId.providerName)
    console.log('provider user ID', providerId.providerUserId)

    if (count > 100) {
    throw new HttpError(403, 'Too many users')
    }

    if (providerId.providerName === 'email' && providerId.providerUserId === 'some@email.com') {
    throw new HttpError(403, 'This email is not allowed')
    }
    }

    Read more about the data the onBeforeSignup hook receives in the API Reference.

    Executing code after the user signs up

    Wasp calls the onAfterSignup hook after the user is created.

    The onAfterSignup hook can be useful if you want to send the user a welcome email or perform some other action after the user signs up like syncing the user with a third-party service.

    Since the onAfterSignup hook receives the OAuth access token, it can also be used to store the OAuth access token for the user in your database.

    Works with Email Username & Password Discord Github Google Keycloak

    main.wasp
    app myApp {
    ...
    auth: {
    ...
    onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
    },
    }
    src/auth/hooks.js
    export const onAfterSignup = async ({
    providerId,
    user,
    oauth,
    prisma,
    req,
    }) => {
    const count = await prisma.user.count()
    console.log('number of users after', count)
    console.log('user object', user)

    // If this is an OAuth signup, we have the access token and uniqueRequestId
    if (oauth) {
    console.log('accessToken', oauth.accessToken)
    console.log('uniqueRequestId', oauth.uniqueRequestId)

    const id = oauth.uniqueRequestId
    const data = someKindOfStore.get(id)
    if (data) {
    console.log('saved data for the ID', data)
    }
    someKindOfStore.delete(id)
    }
    }

    Read more about the data the onAfterSignup hook receives in the API Reference.

    Executing code before the OAuth redirect

    Wasp calls the onBeforeOAuthRedirect hook after the OAuth redirect URL is generated but before redirecting the user. This hook can access the request object sent from the client at the start of the OAuth process.

    The onBeforeOAuthRedirect hook can be useful if you want to save some data (e.g. request query parameters) that can be used later in the OAuth flow. You can use the uniqueRequestId parameter to reference this data later in the onAfterSignup hook.

    Works with Discord Github Google Keycloak

    main.wasp
    app myApp {
    ...
    auth: {
    ...
    onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
    },
    }
    src/auth/hooks.js
    export const onBeforeOAuthRedirect = async ({
    url,
    uniqueRequestId,
    prisma,
    req,
    }) => {
    console.log('query params before oAuth redirect', req.query)

    // Saving query params for later use in the onAfterSignup hook
    const id = uniqueRequestId
    someKindOfStore.set(id, req.query)

    return { url }
    }

    This hook's return value must be an object that looks like this: { url: URL }. Wasp uses the URL to redirect the user to the OAuth provider.

    Read more about the data the onBeforeOAuthRedirect hook receives in the API Reference.

    API Reference

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    auth: {
    userEntity: User,
    methods: {
    ...
    },
    onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
    onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
    onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
    },
    }

    The onBeforeSignup hook

    src/auth/hooks.js
    import { HttpError } from 'wasp/server'

    export const onBeforeSignup = async ({
    providerId,
    prisma,
    req,
    }) => {
    // Hook code here
    }

    The hook receives an object as input with the following properties:

    • providerId: ProviderId

      The user's provider ID is an object with two properties:

      • providerName: string

        The provider's name (e.g. 'email', 'google', 'github')

      • providerUserId: string

        The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID)

    • prisma: PrismaClient

      The Prisma client instance which you can use to query your database.

    • req: Request

      The Express request object from which you can access the request headers, cookies, etc.

    Wasp ignores this hook's return value.

    The onAfterSignup hook

    src/auth/hooks.js
    export const onAfterSignup = async ({
    providerId,
    user,
    oauth,
    prisma,
    req,
    }) => {
    // Hook code here
    }

    The hook receives an object as input with the following properties:

    • providerId: ProviderId

      The user's provider ID is an object with two properties:

      • providerName: string

        The provider's name (e.g. 'email', 'google', 'github')

      • providerUserId: string

      The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID)

    • user: User

      The user object that was created.

    • oauth?: OAuthFields

      This object is present only when the user is created using Social Auth. It contains the following fields:

      • accessToken: string

        You can use the OAuth access token to use the provider's API on user's behalf.

      • uniqueRequestId: string

        The unique request ID for the OAuth flow (you might know it as the state parameter in OAuth.)

        You can use the unique request ID to get the data saved in the onBeforeOAuthRedirect hook.

    • prisma: PrismaClient

      The Prisma client instance which you can use to query your database.

    • req: Request

      The Express request object from which you can access the request headers, cookies, etc.

    Wasp ignores this hook's return value.

    The onBeforeOAuthRedirect hook

    src/auth/hooks.js
    export const onBeforeOAuthRedirect = async ({
    url,
    uniqueRequestId,
    prisma,
    req,
    }) => {
    // Hook code here

    return { url }
    }

    The hook receives an object as input with the following properties:

    • url: URL

      Wasp uses the URL for the OAuth redirect.

    • uniqueRequestId: string

      The unique request ID for the OAuth flow (you might know it as the state parameter in OAuth.)

      You can use the unique request ID to save data (e.g. request query params) that you can later use in the onAfterSignup hook.

    • prisma: PrismaClient

      The Prisma client instance which you can use to query your database.

    • req: Request

      The Express request object from which you can access the request headers, cookies, etc.

    This hook's return value must be an object that looks like this: { url: URL }. Wasp uses the URL to redirect the user to the OAuth provider.

    - - + + \ No newline at end of file diff --git a/docs/auth/email.html b/docs/auth/email.html index ca45232caf..c955c21020 100644 --- a/docs/auth/email.html +++ b/docs/auth/email.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Email

    Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

    Auth UI

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Setting Up Email Authentication

    We'll need to take the following steps to set up email authentication:

    1. Enable email authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages
    5. Set up the email sender

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }

    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Email Authentication in main.wasp

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable email authentication
    email: {
    // 3. Specify the email from field
    fromField: {
    name: "My App Postman",
    email: "hello@itsme.com"
    },
    // 4. Specify the email verification and password reset options (we'll talk about them later)
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    },
    },
    },
    onAuthFailedRedirectTo: "/login",
    onAuthSucceededRedirectTo: "/"
    },
    }

    Read more about the email auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    schema.prisma
    // 5. Define the user entity
    model User {
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    }

    You can read more about how the User is connected to the rest of the auth system and how you can access the user data in the Accessing User Data section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { RequestPasswordReset } from "@src/pages/auth.jsx",
    }

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { PasswordReset } from "@src/pages/auth.jsx",
    }

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { EmailVerification } from "@src/pages/auth.jsx",
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import {
    LoginForm,
    SignupForm,
    VerifyEmailForm,
    ForgotPasswordForm,
    ResetPasswordForm,
    } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    <br />
    <span className="text-sm font-medium text-gray-900">
    Forgot your password? <Link to="/request-password-reset">reset it</Link>
    .
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    export function EmailVerification() {
    return (
    <Layout>
    <VerifyEmailForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    export function RequestPasswordReset() {
    return (
    <Layout>
    <ForgotPasswordForm />
    </Layout>
    );
    }

    export function PasswordReset() {
    return (
    <Layout>
    <ResetPasswordForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    If everything is okay, <Link to="/login">go to login</Link>
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    5. Set up an Email Sender

    To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

    We'll use the Dummy provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the supported email providers.

    To set up the Dummy provider to send emails, add the following to the main.wasp file:

    main.wasp
    app myApp {
    // ...
    // 7. Set up the email sender
    emailSender: {
    provider: Dummy,
    }
    }

    Conclusion

    That's it! We have set up email authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the auth overview.

    Login and Signup Flows

    Login

    Auth UI

    Signup

    Auth UI

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

    3. Allowing registration for unverified emails

      If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

    4. Password validation

      Read more about the default password validation rules and how to override them in auth overview docs.

    Email Verification Flow

    Automatic email verification in development

    In development mode, you can skip the email verification step by setting the SKIP_EMAIL_VERIFICATION_IN_DEV environment variable to true in your .env.server file:

    .env.server
    SKIP_EMAIL_VERIFICATION_IN_DEV=true

    This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app.

    By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

    Our setup looks like this:

    main.wasp
    // ...

    emailVerification: {
    clientRoute: EmailVerificationRoute,
    }

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

    The content of the e-mail can be customized, read more about it here.

    Email Verification Page

    We defined our email verification page in the auth.tsx file.

    Auth UI

    Password Reset Flow

    Users can request a password and then they'll receive an e-mail with a link to reset their password.

    Some of the behavior you get out of the box:

    1. Rate limiting

      We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

    2. Preventing user email leaks

      If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

    Our setup in main.wasp looks like this:

    main.wasp
    // ...

    passwordReset: {
    clientRoute: PasswordResetRoute,
    }

    Request Password Reset Page

    Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

    Request password reset page

    Password Reset Page

    When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

    Request password reset page

    Users can enter their new password there.

    The content of the e-mail can be customized, read more about it here.

    Creating a Custom Sign-up Action

    Creating a custom sign-up action

    We don't recommend creating a custom sign-up action unless you have a good reason to do so. It is a complex process and you can easily make a mistake that will compromise the security of your app.

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidEmail,
    createProviderId,
    sanitizeAndSerializeProviderData,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    createUser,
    createEmailVerificationLink,
    sendEmailVerificationEmail,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidEmail(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('email', args.email)
    const existingAuthIdentity = await findAuthIdentity(providerId)

    if (existingAuthIdentity) {
    const providerData = deserializeAndSanitizeProviderData(existingAuthIdentity.providerData)
    // Your custom code here
    } else {
    // sanitizeAndSerializeProviderData will hash the user's password
    const newUserProviderData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    isEmailVerified: false,
    emailVerificationSentAt: null,
    passwordResetSentAt: null,
    })
    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )

    // Verification link links to a client route e.g. /email-verification
    const verificationLink = await createEmailVerificationLink(args.email, '/email-verification');
    try {
    await sendEmailVerificationEmail(
    args.email,
    {
    from: {
    name: "My App Postman",
    email: "hello@itsme.com",
    },
    to: args.email,
    subject: "Verify your email",
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    }
    );
    } catch (e: unknown) {
    console.error("Failed to send email verification email:", e);
    throw new HttpError(500, "Failed to send email verification email.");
    }
    }
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Email

    • ensureValidEmail(args)

      Checks if the email is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    When you receive the user object on the client or the server, you'll be able to access the user's email and other information like this:

    const emailIdentity = user.identities.email

    // Email address the the user used to sign up, e.g. "fluffyllama@app.com".
    emailIdentity.id

    // `true` if the user has verified their email address.
    emailIdentity.isEmailVerified

    // Datetime when the email verification email was sent.
    emailIdentity.emailVerificationSentAt

    // Datetime when the last password reset email was sent.
    emailIdentity.passwordResetSentAt

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    Let's go over the options we can specify when using email authentication.

    userEntity fields

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    // We'll explain these options below
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    }

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the email dict

    main.wasp
    app myApp {
    title: "My app",
    // ...

    auth: {
    userEntity: User,
    methods: {
    email: {
    userSignupFields: import { userSignupFields } from "@src/auth.js",
    fromField: {
    name: "My App",
    email: "hello@itsme.com"
    },
    emailVerification: {
    clientRoute: EmailVerificationRoute,
    getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js",
    },
    passwordReset: {
    clientRoute: PasswordResetRoute,
    getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js",
    },
    },
    },
    onAuthFailedRedirectTo: "/someRoute"
    },
    // ...
    }

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })

    Read more about the userSignupFields function here.

    fromField: EmailFromField required

    fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

    It has the following fields:

    • name: name of the sender
    • email: e-mail address of the sender required

    emailVerification: EmailVerificationConfig required

    emailVerification is a dict that specifies the details of the e-mail verification process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

      Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

      src/pages/EmailVerificationPage.jsx
      import { verifyEmail } from 'wasp/client/auth'
      ...
      await verifyEmail({ token });
      note

      We used Auth UI above to avoid doing this work of sending the token to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn can be done by defining a file in the src directory.

      src/email.js
      export const getVerificationEmailContent = ({ verificationLink }) => ({
      subject: 'Verify your email',
      text: `Click the link below to verify your email: ${verificationLink}`,
      html: `
      <p>Click the link below to verify your email</p>
      <a href="${verificationLink}">Verify email</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.

    passwordReset: PasswordResetConfig required

    passwordReset is a dict that specifies the password reset process.

    It has the following fields:

    • clientRoute: Route: a route that is used for the user to reset their password. required

      Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

      src/pages/ForgotPasswordPage.jsx
      import { requestPasswordReset } from 'wasp/client/auth'
      ...
      await requestPasswordReset({ email });
      src/pages/PasswordResetPage.jsx
      import { resetPassword } from 'wasp/client/auth'
      ...
      await resetPassword({ password, token })
      note

      We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

    • getEmailContentFn: ExtImport: a function that returns the content of the e-mail that is sent to the user.

      Defining getEmailContentFn is done by defining a function that looks like this:

      src/email.js
      export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
      subject: 'Password reset',
      text: `Click the link below to reset your password: ${passwordResetLink}`,
      html: `
      <p>Click the link below to reset your password</p>
      <a href="${passwordResetLink}">Reset password</a>
      `,
      })
      This is the default content of the e-mail, you can customize it to your liking.
    - - + + \ No newline at end of file diff --git a/docs/auth/entities.html b/docs/auth/entities.html index 0c7c20271c..24b1981e1d 100644 --- a/docs/auth/entities.html +++ b/docs/auth/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Accessing User Data

    First, we'll check out the most practical info: how to access the user's data in your app.

    Then, we'll dive into the details of the auth entities that Wasp creates behind the scenes to store the user's data. For auth each method, Wasp needs to store different information about the user. For example, username for Username & password auth, email verification status for Email auth, and so on.

    We'll also show you how you can use these entities to create a custom signup action.

    Accessing the Auth Fields

    When you receive the user object on the client or the server, it will contain all the user fields you defined in the User entity in the schema.prisma file. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the username or the email verification status. In Wasp, this data is called the AuthUser object.

    AuthUser Object Fields

    All the User fields you defined will be present at the top level of the AuthUser object. The auth-related fields will be on the identities object. For each auth method you enable, there will be a separate data object in the identities object.

    The AuthUser object will change depending on which auth method you have enabled in the Wasp file. For example, if you enabled the email auth and Google auth, it would look something like this:

    If the user has only the Google identity, the AuthUser object will look like this:

    const user = {
    // User data
    id: 'cluqs9qyh00007cn73apj4hp7',
    address: 'Some address',

    // Auth methods specific data
    identities: {
    email: null,
    google: {
    id: '1117XXXX1301972049448',
    },
    },
    }

    In the examples above, you can see the identities object contains the email and google objects. The email object contains the email-related data and the google object contains the Google-related data.

    Make sure to check if the data exists

    Before accessing some auth method's data, you'll need to check if that data exists for the user and then access it:

    if (user.identities.google !== null) {
    const userId = user.identities.google.id
    // ...
    }

    You need to do this because if a user didn't sign up with some auth method, the data for that auth method will be null.

    Let's look at the data for each of the available auth methods:

    • Username & password data

      const usernameIdentity = user.identities.username

      // Username that the user used to sign up, e.g. "fluffyllama"
      usernameIdentity.id
    • Email data

      const emailIdentity = user.identities.email

      // Email address the the user used to sign up, e.g. "fluffyllama@app.com".
      emailIdentity.id

      // `true` if the user has verified their email address.
      emailIdentity.isEmailVerified

      // Datetime when the email verification email was sent.
      emailIdentity.emailVerificationSentAt

      // Datetime when the last password reset email was sent.
      emailIdentity.passwordResetSentAt
    • Google data

      const googleIdentity = user.identities.google

      // Google User ID for example "123456789012345678901"
      googleIdentity.id
    • GitHub data

      const githubIdentity = user.identities.github

      // GitHub User ID for example "12345678"
      githubIdentity.id
    • Keycloak data

      const keycloakIdentity = user.identities.keycloak

      // Keycloak User ID for example "12345678-1234-1234-1234-123456789012"
      keycloakIdentity.id
    • Discord data

      const discordIdentity = user.identities.discord

      // Discord User ID for example "80351110224678912"
      discordIdentity.id

    If you support multiple auth methods, you'll need to find which identity exists for the user and then access its data:

    if (user.identities.email !== null) {
    const email = user.identities.email.id
    // ...
    } else if (user.identities.google !== null) {
    const googleId = user.identities.google.id
    // ...
    }

    getFirstProviderUserId Helper

    The getFirstProviderUserId method returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID.

    This can be useful if you support multiple authentication methods and you need any ID that identifies the user in your app.

    src/MainPage.jsx
    const MainPage = ({ user }) => {
    const userId = user.getFirstProviderUserId()
    // ...
    }
    src/tasks.js
    export const createTask = async (args, context) => {
    const userId = context.user.getFirstProviderUserId()
    // ...
    }

    * Multiple identities per user will be possible in the future and then the getFirstProviderUserId method will return the ID of the first identity that it finds without any guarantees about which one it will be.

    Including the User with Other Entities

    Sometimes, you might want to include the user's data when fetching other entities. For example, you might want to include the user's data with the tasks they have created.

    We'll mention the auth and the identities relations which we will explain in more detail later in the Entities Explained section.

    Be careful about sensitive data

    You'll need to include the auth and the identities relations to get the full auth data about the user. However, you should keep in mind that the providerData field in the identities can contain sensitive data like the user's hashed password (in case of email or username auth), so you will likely want to exclude it if you are returning those values to the client.

    You can include the full user's data with other entities using the include option in the Prisma queries:

    src/tasks.js
    export const getAllTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'desc' },
    select: {
    id: true,
    title: true,
    user: {
    include: {
    auth: {
    include: {
    identities: {
    // Including only the `providerName` and `providerUserId` fields
    select: {
    providerName: true,
    providerUserId: true,
    },
    },
    },
    },
    },
    },
    },
    })
    }

    If you have some piece of the auth data that you want to access frequently (for example the username), it's best to store it at the top level of the User entity.

    For example, save the username or email as a property on the User and you'll be able to access it without including the auth and identities fields. We show an example in the Defining Extra Fields on the User Entity section of the docs.

    Getting Auth Data from the User Object

    When you have the user object with the auth and identities fields, it can be a bit tedious to obtain the auth data (like username or Google ID) from it:

    src/MainPage.jsx
    function MainPage() {
    // ...
    return (
    <div className="tasks">
    {tasks.map((task) => (
    <div key={task.id} className="task">
    {task.title} by {task.user.auth?.identities[0].providerUserId}
    </div>
    ))}
    </div>
    )
    }

    Wasp offers a few helper methods to access the user's auth data when you retrieve the user like this. They are getUsername, getEmail and getFirstProviderUserId. They can be used both on the client and the server.

    getUsername

    It accepts the user object and if the user signed up with the Username & password auth method, it returns the username or null otherwise. The user object needs to have the auth and the identities relations included.

    src/MainPage.jsx
    import { getUsername } from 'wasp/auth'

    function MainPage() {
    // ...
    return (
    <div className="tasks">
    {tasks.map((task) => (
    <div key={task.id} className="task">
    {task.title} by {getUsername(task.user)}
    </div>
    ))}
    </div>
    )
    }

    getEmail

    It accepts the user object and if the user signed up with the Email auth method, it returns the email or null otherwise. The user object needs to have the auth and the identities relations included.

    src/MainPage.jsx
    import { getEmail } from 'wasp/auth'

    function MainPage() {
    // ...
    return (
    <div className="tasks">
    {tasks.map((task) => (
    <div key={task.id} className="task">
    {task.title} by {getEmail(task.user)}
    </div>
    ))}
    </div>
    )
    }

    getFirstProviderUserId

    It returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID. The user object needs to have the auth and the identities relations included.

    src/MainPage.jsx
    import { getFirstProviderUserId } from 'wasp/auth'

    function MainPage() {
    // ...
    return (
    <div className="tasks">
    {tasks.map((task) => (
    <div key={task.id} className="task">
    {task.title} by {getFirstProviderUserId(task.user)}
    </div>
    ))}
    </div>
    )
    }

    Entities Explained

    To store user's auth information, Wasp does a few things behind the scenes. Wasp takes your schema.prisma file and combines it with additional entities to create the final schema.prisma file that is used in your app.

    In this section, we will explain which entities are created and how they are connected.

    User Entity

    When you want to add authentication to your app, you need to specify the userEntity field.

    For example, you might set it to User:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    // ...
    },
    }

    And define the User in the schema.prisma file:

    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    // Any other fields you want to store about the user
    }

    The User entity is a "business logic user" which represents a user of your app.

    You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address.

    You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created.

    You own the user entity and you can modify it as you wish. You can add new fields to it, remove fields from it, or change the type of the fields. You can also add new relations to it or remove existing relations from it.

    Auth Entities in a Wasp App
    Auth Entities in a Wasp App

    On the other hand, the Auth, AuthIdentity and Session entities are created behind the scenes and are used to store the user's login credentials. You as the developer don't need to care about this entity most of the time. Wasp owns these entities.

    In the case you want to create a custom signup action, you will need to use the Auth and AuthIdentity entities directly.

    Example App Model

    Let's imagine we created a simple tasks management app:

    • The app has email and Google-based auth.
    • Users can create tasks and see the tasks that they have created.

    Let's look at how would that look in the database:

    Example of Auth Entities
    Example of Auth Entities

    If we take a look at an example user in the database, we can see:

    • The business logic user, User is connected to multiple Task entities.
      • In this example, "Example User" has two tasks.
    • The User is connected to exactly one Auth entity.
    • Each Auth entity can have multiple AuthIdentity entities.
      • In this example, the Auth entity has two AuthIdentity entities: one for the email-based auth and one for the Google-based auth.
    • Each Auth entity can have multiple Session entities.
      • In this example, the Auth entity has one Session entity.
    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Auth Entity internal

    Wasp's internal Auth entity is used to connect the business logic user, User with the user's login credentials.

    model Auth {
    id String @id @default(uuid())
    userId Int? @unique
    // Wasp injects this relation on the User entity as well
    user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
    identities AuthIdentity[]
    sessions Session[]
    }

    The Auth fields:

    • id is a unique identifier of the Auth entity.
    • userId is a foreign key to the User entity.
      • It is used to connect the Auth entity with the business logic user.
    • user is a relation to the User entity.
      • This relation is injected on the User entity as well.
    • identities is a relation to the AuthIdentity entity.
    • sessions is a relation to the Session entity.

    AuthIdentity Entity internal

    The AuthIdentity entity is used to store the user's login credentials for various authentication methods.

    model AuthIdentity {
    providerName String
    providerUserId String
    providerData String @default("{}")
    authId String
    auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade)

    @@id([providerName, providerUserId])
    }

    The AuthIdentity fields:

    • providerName is the name of the authentication provider.
      • For example, email or google.
    • providerUserId is the user's ID in the authentication provider.
      • For example, the user's email or Google ID.
    • providerData is a JSON string that contains additional data about the user from the authentication provider.
    • authId is a foreign key to the Auth entity.
      • It is used to connect the AuthIdentity entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Session Entity internal

    The Session entity is used to store the user's session information. It is used to keep the user logged in between page refreshes.

    model Session {
    id String @id @unique
    expiresAt DateTime
    userId String
    auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade)

    @@index([userId])
    }

    The Session fields:

    • id is a unique identifier of the Session entity.
    • expiresAt is the date when the session expires.
    • userId is a foreign key to the Auth entity.
      • It is used to connect the Session entity with the Auth entity.
    • auth is a relation to the Auth entity.

    Custom Signup Action

    Let's take a look at how you can use the Auth and AuthIdentity entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service.

    Custom Signup Examples

    In the Email section of the docs we give you an example for custom email signup and in the Username & password section of the docs we give you an example for custom username & password signup.

    Below is a simplified version of a custom signup action which you probably wouldn't use in your app but it shows you how you can use the Auth and AuthIdentity entities to create a custom signup action.

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    entities: [User]
    }
    src/auth/signup.js
    import {
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, { entities: { User } }) => {
    try {
    // Provider ID is a combination of the provider name and the provider user ID
    // And it is used to uniquely identify the user in your app
    const providerId = createProviderId('username', args.username)
    // sanitizeAndSerializeProviderData hashes the password and returns a JSON string
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {}
    )

    // This is equivalent to:
    // await User.create({
    // data: {
    // auth: {
    // create: {
    // identities: {
    // create: {
    // providerName: 'username',
    // providerUserId: args.username
    // providerData,
    // },
    // },
    // }
    // },
    // }
    // })
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    You can use whichever method suits your needs better: either the createUser function or Prisma's User.create method. The createUser function is a bit more convenient to use because it hides some of the complexity. On the other hand, the User.create method gives you more control over the data that is stored in the Auth and AuthIdentity entities.

    - - + + \ No newline at end of file diff --git a/docs/auth/overview.html b/docs/auth/overview.html index 28bee19b4e..b57aeaad1c 100644 --- a/docs/auth/overview.html +++ b/docs/auth/overview.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Overview

    Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box.

    Here's a 1-minute tour of how full-stack auth works in Wasp:

    Enabling auth for your app is optional and can be done by configuring the auth field of your app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute"
    }
    }

    //...

    Read more about the auth field options in the API Reference section.

    We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.

    Available auth methods

    Wasp supports the following auth methods:

    Click on each auth method for more details.

    Let's say we enabled the Username & password authentication.

    We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.

    We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.

    We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.

    Protecting a page with authRequired

    When declaring a page, you can set the authRequired property.

    If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.

    main.wasp
    page MainPage {
    component: import Main from "@src/pages/Main",
    authRequired: true
    }
    Requires auth method

    You can only use authRequired if your app uses one of the available auth methods.

    If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.

    Logout action

    We provide an action for logging out the user. Here's how you can use it:

    src/components/LogoutButton.jsx
    import { logout } from 'wasp/client/auth'

    const LogoutButton = () => {
    return <button onClick={logout}>Logout</button>
    }

    Accessing the logged-in user

    You can get access to the user object both on the server and on the client. The user object contains the logged-in user's data.

    The user object has all the fields that you defined in your User entity. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the username or the email verification status. For example, if you have a user that signed up using an email and password, the user object might look like this:

    const user = {
    // User data
    id: "cluqsex9500017cn7i2hwsg17",
    address: "Some address",

    // Auth methods specific data
    identities: {
    email: {
    id: "user@app.com",
    isEmailVerified: true,
    emailVerificationSentAt: "2024-04-08T10:06:02.204Z",
    passwordResetSentAt: null,
    },
    },
    }

    You can read more about how the User is connected to the rest of the auth system and how you can access the user data in the Accessing User Data section of the docs.

    On the client

    There are two ways to access the user object on the client:

    • the user prop
    • the useAuth hook

    Using the user prop

    If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:

    main.wasp
    // ...

    page AccountPage {
    component: import Account from "@src/pages/Account",
    authRequired: true
    }
    src/pages/Account.jsx
    import Button from './Button'
    import { logout } from 'wasp/client/auth'

    const AccountPage = ({ user }) => {
    return (
    <div>
    <Button onClick={logout}>Logout</Button>
    {JSON.stringify(user, null, 2)}
    </div>
    )
    }

    export default AccountPage

    Using the useAuth hook

    Wasp provides a React hook you can use in the client components - useAuth.

    This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.

    src/pages/MainPage.jsx
    import { useAuth, logout } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'
    import Todo from '../Todo'

    export function Main() {
    const { data: user } = useAuth()

    if (!user) {
    return (
    <span>
    Please <Link to="/login">login</Link> or{' '}
    <Link to="/signup">sign up</Link>.
    </span>
    )
    } else {
    return (
    <>
    <button onClick={logout}>Logout</button>
    <Todo />
    </>
    )
    }
    }
    tip

    Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.

    On the server

    Using the context.user object

    When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields and the auth identities connected to the user. We strip out the hashedPassword field from the identities for security reasons.

    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (task, context) => {
    if (!context.user) {
    throw new HttpError(403)
    }

    const Task = context.entities.Task
    return Task.create({
    data: {
    description: task.description,
    user: {
    connect: { id: context.user.id },
    },
    },
    })
    }

    To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.

    When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.

    Sessions

    Wasp's auth uses sessions to keep track of the logged-in user. The session is stored in localStorage on the client and in the database on the server. Under the hood, Wasp uses the excellent Lucia Auth v3 library for session management.

    When users log in, Wasp creates a session for them and stores it in the database. The session is then sent to the client and stored in localStorage. When users log out, Wasp deletes the session from the database and from localStorage.

    User Entity

    Password Hashing

    If you are saving a user's password in the database, you should never save it as plain text. You can use Wasp's helper functions for serializing and deserializing provider data which will automatically hash the password for you:

    main.wasp
    // ...

    action updatePassword {
    fn: import { updatePassword } from "@src/auth",
    }
    src/auth.js
    import {
    createProviderId,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    deserializeAndSanitizeProviderData,
    } from 'wasp/server/auth';

    export const updatePassword = async (args, context) => {
    const providerId = createProviderId('email', args.email)
    const authIdentity = await findAuthIdentity(providerId)
    if (!authIdentity) {
    throw new HttpError(400, "Unknown user")
    }

    const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData)

    // Updates the password and hashes it automatically.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: args.password,
    })
    }

    Default Validations

    When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in Auth UI or if you use the provided auth actions.

    If you decide to create your custom auth actions, you'll need to run the validations yourself.

    Default validations depend on the auth method you use.

    Username & Password

    If you use Username & password authentication, the default validations are:

    • The username must not be empty
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that usernames are stored in a case-insensitive manner.

    Email

    If you use Email authentication, the default validations are:

    • The email must not be empty and a valid email address
    • The password must not be empty, have at least 8 characters, and contain a number

    Note that emails are stored in a case-insensitive manner.

    Customizing the Signup Process

    Sometimes you want to include extra fields in your signup process, like first name and last name and save them in the User entity.

    For this to happen:

    • you need to define the fields that you want saved in the database,
    • you need to customize the SignupForm (in the case of Email or Username & Password auth)

    Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.

    Let's see how to do both.

    1. Defining Extra Fields

    If we want to save some extra fields in our signup process, we need to tell our app they exist.

    We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    * We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

    First, we add the auth.methods.{authMethod}.userSignupFields field in our main.wasp file. The {authMethod} depends on the auth method you are using.

    For example, if you are using Username & Password, you would add the auth.methods.usernameAndPassword.userSignupFields field:

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    address String?
    }

    Then we'll define the userSignupFields object in the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    Read more about the userSignupFields object in the API Reference.

    Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity you defined in the schema.prisma file.

    The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.

    Using Validation Libraries

    You can use any validation library you want to validate the fields. For example, you can use zod like this:

    Click to see the code
    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'
    import * as z from 'zod'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    const AddressSchema = z
    .string({
    required_error: 'Address is required',
    invalid_type_error: 'Address must be a string',
    })
    .min(10, 'Address must be at least 10 characters long')
    const result = AddressSchema.safeParse(data.address)
    if (result.success === false) {
    throw new Error(result.error.issues[0].message)
    }
    return result.data
    },
    })

    Now that we defined the fields, Wasp knows how to:

    1. Validate the data sent from the client
    2. Save the data to the database

    Next, let's see how to customize Auth UI to include those fields.

    2. Customizing the Signup Component

    Using Custom Signup Component

    If you are not using Wasp's Auth UI, you can skip this section. Just make sure to include the extra fields in your custom signup form.

    Read more about using the signup actions for:

    • email auth here
    • username & password auth here

    If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.

    Using a List of Extra Fields

    When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.

    Inside the list, there can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
    2. Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the react-hook-form object and the form state object as arguments.
    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    /* The address field is defined using an object */
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    /* The phone number is defined using a render function */
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    Read more about the extra fields in the API Reference.

    Using a Single Render Function

    Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.

    src/SignupPage.jsx
    import { SignupForm, FormItemGroup } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={(form, state) => {
    const username = form.watch('username')
    return (
    username && (
    <FormItemGroup>
    Hello there <strong>{username}</strong> 👋
    </FormItemGroup>
    )
    )
    }}
    />
    )
    }

    Read more about the render function in the API Reference.

    API Reference

    Auth Fields

    main.wasp
      title: "My app",
    //...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {}, // use this or email, not both
    email: {}, // use this or usernameAndPassword, not both
    google: {},
    gitHub: {},
    },
    onAuthFailedRedirectTo: "/someRoute",
    }
    }

    //...

    app.auth is a dictionary with the following fields:

    userEntity: entity required

    The entity representing the user connected to your business logic.

    You can read more about how the User is connected to the rest of the auth system and how you can access the user data in the Accessing User Data section of the docs.

    methods: dict required

    A dictionary of auth methods enabled for the app.

    Click on each auth method for more details.

    onAuthFailedRedirectTo: String required

    The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true). Check out these essential docs on auth to see an example of usage.

    onAuthSucceededRedirectTo: String

    The route to which Wasp will send a successfully authenticated after a successful login/signup. The default value is "/".

    note

    Automatic redirect on successful login only works when using the Wasp-provided Auth UI.

    Signup Fields Customization

    If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.methods.{authMethod}.userSignupFields field in your main.wasp file.

    main.wasp
    app crudTesting {
    // ...
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/signup",
    },
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    Then we'll export the userSignupFields object from the src/auth/signup.js file:

    src/auth/signup.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: async (data) => {
    const address = data.address
    if (typeof address !== 'string') {
    throw new Error('Address is required')
    }
    if (address.length < 5) {
    throw new Error('Address must be at least 5 characters long')
    }
    return address
    },
    })

    The userSignupFields object is an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

    If the value that the function received is invalid, the function should throw an error.

    * We exclude the password field from this object to prevent it from being saved as plain text in the database. The password field is handled by Wasp's auth backend.

    SignupForm Customization

    To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.

    src/SignupPage.jsx
    import {
    SignupForm,
    FormError,
    FormInput,
    FormItemGroup,
    FormLabel,
    } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <SignupForm
    additionalFields={[
    {
    name: 'address',
    label: 'Address',
    type: 'input',
    validations: {
    required: 'Address is required',
    },
    },
    (form, state) => {
    return (
    <FormItemGroup>
    <FormLabel>Phone Number</FormLabel>
    <FormInput
    {...form.register('phoneNumber', {
    required: 'Phone number is required',
    })}
    disabled={state.isLoading}
    />
    {form.formState.errors.phoneNumber && (
    <FormError>
    {form.formState.errors.phoneNumber.message}
    </FormError>
    )}
    </FormItemGroup>
    )
    },
    ]}
    />
    )
    }

    The extra fields can be either objects or render functions (you can combine them):

    1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.

      The objects have the following properties:

      • name required

        • the name of the field
      • label required

        • the label of the field (used in the UI)
      • type required

        • the type of the field, which can be input or textarea
      • validations

        • an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
    2. Render functions receive the react-hook-form object and the form state as arguments, and they can use them to render arbitrary UI elements.

      The render function has the following signature:

      (form: UseFormReturn, state: FormState) => React.ReactNode
      • form required

        • the react-hook-form object, read more about it in the react-hook-form docs
        • you need to use the form.register function to register your fields
      • state required

        • the form state object which has the following properties:
          • isLoading: boolean
            • whether the form is currently submitting
    - - + + \ No newline at end of file diff --git a/docs/auth/social-auth/discord.html b/docs/auth/social-auth/discord.html index 6fa815efaf..33c9c7d8d4 100644 --- a/docs/auth/social-auth/discord.html +++ b/docs/auth/social-auth/discord.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Discord

    Wasp supports Discord Authentication out of the box.

    Letting your users log in using their Discord accounts turns the signup process into a breeze.

    Let's walk through enabling Discord Authentication, explain some of the default settings, and show how to override them.

    Setting up Discord Auth

    Enabling Discord Authentication comes down to a series of steps:

    1. Enabling Discord authentication in the Wasp file.
    2. Adding the User entity.
    3. Creating a Discord App.
    4. Adding the necessary Routes and Pages
    5. Using Auth UI components in our Pages.

    Here's a skeleton of how our main.wasp should look like after we're done:

    main.wasp
    // Configuring the social authentication
    app myApp {
    auth: { ... }
    }

    // Defining routes and pages
    route LoginRoute { ... }
    page LoginPage { ... }

    1. Adding Discord Auth to Your Wasp File

    Let's start by properly configuring the Auth object:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the User entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable Discord Auth
    discord: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    2. Add the User Entity

    Let's now define the app.auth.userEntity entity in the schema.prisma file:

    schema.prisma
    // 3. Define the user entity
    model User {
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    }

    3. Creating a Discord App

    To use Discord as an authentication method, you'll first need to create a Discord App and provide Wasp with your client key and secret. Here's how you do it:

    1. Log into your Discord account and navigate to: https://discord.com/developers/applications.
    2. Select New Application.
    3. Supply required information.
    Discord Applications Screenshot
    1. Go to the OAuth2 tab on the sidebar and click Add Redirect
    • For development, put: http://localhost:3001/auth/discord/callback.
    • Once you know on which URL your API server will be deployed, you can create a new app with that URL instead e.g. https://your-server-url.com/auth/discord/callback.
    1. Hit Save Changes.
    2. Hit Reset Secret.
    3. Copy your Client ID and Client secret as you'll need them in the next step.

    4. Adding Environment Variables

    Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

    .env.server
    DISCORD_CLIENT_ID=your-discord-client-id
    DISCORD_CLIENT_SECRET=your-discord-client-secret

    5. Adding the Necessary Routes and Pages

    Let's define the necessary authentication Routes and Pages.

    Add the following code to your main.wasp file:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    6. Creating the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    Yay, we've successfully set up Discord Auth! 🎉

    Discord Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add discord: {} to the auth.methods dictionary to use it with default settings.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    discord: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From Discord

    We are using Discord's API and its /users/@me endpoint to get the user data.

    The data we receive from Discord on the /users/@me endpoint looks something like this:

    {
    "id": "80351110224678912",
    "username": "Nelly",
    "discriminator": "1337",
    "avatar": "8342729096ea3675442027381ff50dfe",
    "verified": true,
    "flags": 64,
    "banner": "06c16474723fe537c283b8efa61a30c8",
    "accent_color": 16711680,
    "premium_type": 1,
    "public_flags": 64,
    "avatar_decoration_data": {
    "sku_id": "1144058844004233369",
    "asset": "a_fed43ab12698df65902ba06727e20c0e"
    }
    }

    The fields you receive will depend on the scopes you requested. The default scope is set to identify only. If you want to get the email, you need to specify the email scope in the configFn function.

    For an up to date info about the data received from Discord, please refer to the Discord API documentation.

    Using the Data Received From Discord

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    discord: {
    configFn: import { getConfig } from "@src/auth/discord.js",
    userSignupFields: import { userSignupFields } from "@src/auth/discord.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    }

    // ...
    src/auth/discord.js
    export const userSignupFields = {
    username: (data) => data.profile.global_name,
    avatarUrl: (data) => data.profile.avatar,
    };

    export function getConfig() {
    return {
    scopes: ['identify'],
    };
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    When you receive the user object on the client or the server, you'll be able to access the user's Discord ID like this:

    const discordIdentity = user.identities.discord

    // Discord User ID for example "80351110224678912"
    discordIdentity.id

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    discord: {
    configFn: import { getConfig } from "@src/auth/discord.js",
    userSignupFields: import { userSignupFields } from "@src/auth/discord.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The discord dict has the following properties:

    • configFn: ExtImport

      This function should return an object with the scopes for the OAuth provider.

      src/auth/discord.js
      export function getConfig() {
      return {
      scopes: [],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/auth/social-auth/github.html b/docs/auth/social-auth/github.html index 86a8ee1dfc..443ba52fe8 100644 --- a/docs/auth/social-auth/github.html +++ b/docs/auth/social-auth/github.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.

    Letting your users log in using their GitHub accounts turns the signup process into a breeze.

    Let's walk through enabling Github Authentication, explain some of the default settings, and show how to override them.

    Setting up Github Auth

    Enabling GitHub Authentication comes down to a series of steps:

    1. Enabling GitHub authentication in the Wasp file.
    2. Adding the User entity.
    3. Creating a GitHub OAuth app.
    4. Adding the necessary Routes and Pages
    5. Using Auth UI components in our Pages.

    Here's a skeleton of how our main.wasp should look like after we're done:

    main.wasp
    // Configuring the social authentication
    app myApp {
    auth: { ... }
    }

    // Defining routes and pages
    route LoginRoute { ... }
    page LoginPage { ... }

    1. Adding Github Auth to Your Wasp File

    Let's start by properly configuring the Auth object:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the User entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable Github Auth
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    2. Add the User Entity

    Let's now define the app.auth.userEntity entity in the schema.prisma file:

    schema.prisma
    // 3. Define the user entity
    model User {
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    }

    3. Creating a GitHub OAuth App

    To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it:

    1. Log into your GitHub account and navigate to: https://github.com/settings/developers.
    2. Select New OAuth App.
    3. Supply required information.
    GitHub Applications Screenshot
    • For Authorization callback URL:
      • For development, put: http://localhost:3001/auth/github/callback.
      • Once you know on which URL your API server will be deployed, you can create a new app with that URL instead e.g. https://your-server-url.com/auth/github/callback.
    1. Hit Register application.
    2. Hit Generate a new client secret on the next page.
    3. Copy your Client ID and Client secret as you'll need them in the next step.

    4. Adding Environment Variables

    Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

    .env.server
    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret

    5. Adding the Necessary Routes and Pages

    Let's define the necessary authentication Routes and Pages.

    Add the following code to your main.wasp file:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    6. Creating the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    Yay, we've successfully set up Github Auth! 🎉

    Github Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add gitHub: {} to the auth.methods dictionary to use it with default settings.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From GitHub

    We are using GitHub's API and its /user and /user/emails endpoints to get the user data.

    We combine the data from the two endpoints

    You'll find the emails in the emails property in the object that you receive in userSignupFields.

    This is because we combine the data from the /user and /user/emails endpoints if the user or user:email scope is requested.

    The data we receive from GitHub on the /user endpoint looks something this:

    {
    "login": "octocat",
    "id": 1,
    "name": "monalisa octocat",
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    // ...
    }

    And the data from the /user/emails endpoint looks something like this:

    [
    {
    "email": "octocat@github.com",
    "verified": true,
    "primary": true,
    "visibility": "public"
    }
    ]

    The fields you receive will depend on the scopes you requested. By default we don't specify any scopes. If you want to get the emails, you need to specify the user or user:email scope in the configFn function.

    For an up to date info about the data received from GitHub, please refer to the GitHub API documentation.

    Using the Data Received From GitHub

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    }

    // ...
    src/auth/github.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    };

    export function getConfig() {
    return {
    scopes: ['user'],
    };
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    When you receive the user object on the client or the server, you'll be able to access the user's GitHub ID like this:

    const githubIdentity = user.identities.github

    // GitHub User ID for example "12345678"
    githubIdentity.id

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    gitHub: {
    configFn: import { getConfig } from "@src/auth/github.js",
    userSignupFields: import { userSignupFields } from "@src/auth/github.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The gitHub dict has the following properties:

    • configFn: ExtImport

      This function should return an object with the scopes for the OAuth provider.

      src/auth/github.js
      export function getConfig() {
      return {
      scopes: [],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/auth/social-auth/google.html b/docs/auth/social-auth/google.html index c9eddba9d1..ca37f5e5e9 100644 --- a/docs/auth/social-auth/google.html +++ b/docs/auth/social-auth/google.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ It should have the following code:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }
    Auth UI

    Our pages use an automatically-generated Auth UI component. Read more about Auth UI components here.

    Conclusion

    Yay, we've successfully set up Google Auth! 🎉

    Google Auth

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add google: {} to the auth.methods dictionary to use it with default settings:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From Google

    We are using Google's API and its /userinfo endpoint to fetch the user's data.

    The data received from Google is an object which can contain the following fields:

    [
    "name",
    "given_name",
    "family_name",
    "email",
    "email_verified",
    "aud",
    "exp",
    "iat",
    "iss",
    "locale",
    "picture",
    "sub"
    ]

    The fields you receive depend on the scopes you request. The default scope is set to profile only. If you want to get the user's email, you need to specify the email scope in the configFn function.

    For an up to date info about the data received from Google, please refer to the Google API documentation.

    Using the Data Received From Google

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    }

    // ...
    src/auth/google.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    }

    export function getConfig() {
    return {
    scopes: ['profile', 'email'],
    }
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    When you receive the user object on the client or the server, you'll be able to access the user's Google ID like this:

    const googleIdentity = user.identities.google

    // Google User ID for example "123456789012345678901"
    googleIdentity.id

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    google: {
    configFn: import { getConfig } from "@src/auth/google.js",
    userSignupFields: import { userSignupFields } from "@src/auth/google.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The google dict has the following properties:

    • configFn: ExtImport

      This function must return an object with the scopes for the OAuth provider.

      src/auth/google.js
      export function getConfig() {
      return {
      scopes: ['profile', 'email'],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/auth/social-auth/keycloak.html b/docs/auth/social-auth/keycloak.html index 1b3d491b0c..5585857b43 100644 --- a/docs/auth/social-auth/keycloak.html +++ b/docs/auth/social-auth/keycloak.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ It should have the following code:

    src/pages/auth.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    </Layout>
    )
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    )
    }
    Auth UI

    Our pages use an automatically generated Auth UI component. Read more about Auth UI components here.

    Conclusion

    Yay, we've successfully set up Keycloak Auth!

    Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

    Default Behaviour

    Add keycloak: {} to the auth.methods dictionary to use it with default settings:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {}
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

    Overrides

    By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider.

    There are two mechanisms used for overriding the default behavior:

    • userSignupFields
    • configFn

    Let's explore them in more detail.

    Data Received From Keycloak

    We are using Keycloak's API and its /userinfo endpoint to fetch the user's data.

    Keycloak user data
    {
    sub: '5adba8fc-3ea6-445a-a379-13f0bb0b6969',
    email_verified: true,
    name: 'Test User',
    preferred_username: 'test',
    given_name: 'Test',
    family_name: 'User',
    email: 'test@example.com'
    }

    The fields you receive will depend on the scopes you requested. The default scope is set to profile only. If you want to get the user's email, you need to specify the email scope in the configFn function.

    For up-to-date info about the data received from Keycloak, please refer to the Keycloak API documentation.

    Using the Data Received From Keycloak

    When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the userSignupFields getters.

    For example, the User entity can include a displayName field which you can set based on the details received from the provider.

    Wasp also lets you customize the configuration of the providers' settings using the configFn function.

    Let's use this example to show both fields in action:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {
    configFn: import { getConfig } from "@src/auth/keycloak.js",
    userSignupFields: import { userSignupFields } from "@src/auth/keycloak.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    username String @unique
    displayName String
    }

    // ...
    src/auth/keycloak.js
    export const userSignupFields = {
    username: () => "hardcoded-username",
    displayName: (data) => data.profile.name,
    }

    export function getConfig() {
    return {
    scopes: ['profile', 'email'],
    }
    }

    Using Auth

    To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

    When you receive the user object on the client or the server, you'll be able to access the user's Keycloak ID like this:

    const keycloakIdentity = user.identities.keycloak

    // Keycloak User ID for example "12345678-1234-1234-1234-123456789012"
    keycloakIdentity.id

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    Provider-specific behavior comes down to implementing two functions.

    • configFn
    • userSignupFields

    The reference shows how to define both.

    For behavior common to all providers, check the general API Reference.

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    keycloak: {
    configFn: import { getConfig } from "@src/auth/keycloak.js",
    userSignupFields: import { userSignupFields } from "@src/auth/keycloak.js"
    }
    },
    onAuthFailedRedirectTo: "/login"
    },
    }

    The keycloak dict has the following properties:

    • configFn: ExtImport

      This function must return an object with the scopes for the OAuth provider.

      src/auth/keycloak.js
      export function getConfig() {
      return {
      scopes: ['profile', 'email'],
      }
      }
    • userSignupFields: ExtImport

      userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

      src/auth.js
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      address: (data) => {
      if (!data.address) {
      throw new Error('Address is required')
      }
      return data.address
      }
      phone: (data) => data.phone,
      })

      Read more about the userSignupFields function here.

    - - + + \ No newline at end of file diff --git a/docs/auth/social-auth/overview.html b/docs/auth/social-auth/overview.html index 123c09ed65..61450e32a4 100644 --- a/docs/auth/social-auth/overview.html +++ b/docs/auth/social-auth/overview.html @@ -19,8 +19,8 @@ - - + +
    @@ -31,7 +31,7 @@ Depending on the flag's value, you can redirect users to the appropriate signup step.

    For example:

    1. When the user lands on the homepage, check the value of user.isSignupComplete.
    2. If it's false, it means the user has started the signup process but hasn't yet chosen their username. Therefore, you can redirect them to EditUserDetailsPage where they can edit the username property.
    src/HomePage.jsx
    import { useAuth } from 'wasp/client/auth'
    import { Redirect } from 'react-router-dom'

    export function HomePage() {
    const { data: user } = useAuth()

    if (user.isSignupComplete === false) {
    return <Redirect to="/edit-user-details" />
    }

    // ...
    }

    Using the User's Provider Account Details

    Account details are provider-specific. Each provider has their own rules for defining the userSignupFields and configFn fields:

    UI Helpers

    Use Auth UI

    Auth UI is a common name for all high-level auth forms that come with Wasp.

    These include fully functional auto-generated login and signup forms with working social login buttons. If you're looking for the fastest way to get your auth up and running, that's where you should look.

    The UI helpers described below are lower-level and are useful for creating your custom forms.

    Wasp provides sign-in buttons and URLs for each of the supported social login providers.

    src/LoginPage.jsx
    import {
    GoogleSignInButton,
    googleSignInUrl,
    GitHubSignInButton,
    gitHubSignInUrl,
    } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <>
    <GoogleSignInButton />
    <GitHubSignInButton />
    {/* or */}
    <a href={googleSignInUrl}>Sign in with Google</a>
    <a href={gitHubSignInUrl}>Sign in with GitHub</a>
    </>
    )
    }

    If you need even more customization, you can create your custom components using signInUrls.

    API Reference

    Fields in the app.auth Dictionary and Overrides

    For more information on:

    • Allowed fields in app.auth
    • userSignupFields and configFn functions

    Check the provider-specific API References:

    - - + + \ No newline at end of file diff --git a/docs/auth/ui.html b/docs/auth/ui.html index 1c35be553b..af434ed61a 100644 --- a/docs/auth/ui.html +++ b/docs/auth/ui.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Auth UI

    To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

    Below we cover all of the available UI components and how to use them.

    Auth UI

    Overview

    After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

    Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    },
    // ...
    }
    }

    You'll get the following UI:

    Auth UI

    And then if you enable Google and Github:

    main.wasp
    app MyApp {
    //...
    auth: {
    methods: {
    email: {},
    google: {},
    github: {},
    },
    // ...
    }
    }

    The form will automatically update to look like this:

    Auth UI

    Let's go through all of the available components and how to use them.

    Auth Components

    The following components are available for you to use in your app:

    Login Form

    Used with Username & Password, Email, Github, Google, Keycloak, and Discord authentication.

    Login form

    You can use the LoginForm component to build your login page:

    main.wasp
    // ...

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage.jsx"
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    // Use it like this
    export function LoginPage() {
    return <LoginForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Signup Form

    Used with Username & Password, Email, Github, Google, Keycloak, and Discord authentication.

    Signup form

    You can use the SignupForm component to build your signup page:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage.jsx"
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    // Use it like this
    export function SignupPage() {
    return <SignupForm />
    }

    It will automatically show the correct authentication providers based on your main.wasp file.

    Read more about customizing the signup process like adding additional fields or extra UI in the Auth Overview section.

    Forgot Password Form

    Used with Email authentication.

    If users forget their password, they can use this form to reset it.

    Forgot password form

    You can use the ForgotPasswordForm component to build your own forgot password page:

    main.wasp
    // ...

    route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
    page RequestPasswordResetPage {
    component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage.jsx"
    }
    src/ForgotPasswordPage.jsx
    import { ForgotPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ForgotPasswordPage() {
    return <ForgotPasswordForm />
    }

    Reset Password Form

    Used with Email authentication.

    After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

    Reset password form

    You can use the ResetPasswordForm component to build your reset password page:

    main.wasp
    // ...

    route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
    page PasswordResetPage {
    component: import { ResetPasswordPage } from "@src/ResetPasswordPage.jsx"
    }
    src/ResetPasswordPage.jsx
    import { ResetPasswordForm } from 'wasp/client/auth'

    // Use it like this
    export function ResetPasswordPage() {
    return <ResetPasswordForm />
    }

    Verify Email Form

    Used with Email authentication.

    After users sign up, they will receive an email with a link to this form where they can verify their email.

    Verify email form

    You can use the VerifyEmailForm component to build your email verification page:

    main.wasp
    // ...

    route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
    page EmailVerificationPage {
    component: import { VerifyEmailPage } from "@src/VerifyEmailPage.jsx"
    }
    src/VerifyEmailPage.jsx
    import { VerifyEmailForm } from 'wasp/client/auth'

    // Use it like this
    export function VerifyEmailPage() {
    return <VerifyEmailForm />
    }

    Customization 💅🏻

    You customize all of the available forms by passing props to them.

    Props you can pass to all of the forms:

    1. appearance - customize the form colors (via design tokens)
    2. logo - path to your logo
    3. socialLayout - layout of the social buttons, which can be vertical or horizontal

    1. Customizing the Colors

    We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

    List of all available tokens

    See the list of all available tokens which you can override.

    src/appearance.js
    export const authAppearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { authAppearance } from './appearance'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass the appearance object to the form
    appearance={authAppearance}
    />
    )
    }

    We recommend defining your appearance in a separate file and importing it into your components.

    You can add your logo to the Auth UI by passing the logo prop to any of the components.

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import Logo from './logo.png'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the path to your logo
    logo={Logo}
    />
    )
    }

    3. Social Buttons Layout

    You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

    If we pass in vertical:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    export function LoginPage() {
    return (
    <LoginForm
    // Pass in the socialLayout prop
    socialLayout="vertical"
    />
    )
    }

    We get this:

    Vertical social buttons

    Let's Put Everything Together 🪄

    If we provide the logo and custom colors:

    src/appearance.js
    export const appearance = {
    colors: {
    brand: '#5969b8', // blue
    brandAccent: '#de5998', // pink
    submitButtonText: 'white',
    },
    }
    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'

    import { authAppearance } from './appearance'
    import todoLogo from './todoLogo.png'

    export function LoginPage() {
    return <LoginForm appearance={appearance} logo={todoLogo} />
    }

    We get a form looking like this:

    Custom login form
    - - + + \ No newline at end of file diff --git a/docs/auth/username-and-pass.html b/docs/auth/username-and-pass.html index fbd8e54fc5..bae64e6e01 100644 --- a/docs/auth/username-and-pass.html +++ b/docs/auth/username-and-pass.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Username & Password

    Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client side.

    Setting Up Username & Password Authentication

    To set up username authentication we need to:

    1. Enable username authentication in the Wasp file
    2. Add the User entity
    3. Add the auth routes and pages
    4. Use Auth UI components in our pages

    Structure of the main.wasp file we will end up with:

    main.wasp
    // Configuring e-mail authentication
    app myApp {
    auth: { ... }
    }

    // Defining routes and pages
    route SignupRoute { ... }
    page SignupPage { ... }
    // ...

    1. Enable Username Authentication

    Let's start with adding the following to our main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    // 1. Specify the user entity (we'll define it next)
    userEntity: User,
    methods: {
    // 2. Enable username authentication
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }

    Read more about the usernameAndPassword auth method options here.

    2. Add the User Entity

    The User entity can be as simple as including only the id field:

    schema.prisma
    // 3. Define the user entity
    model User {
    id Int @id @default(autoincrement())
    // Add your own fields below
    // ...
    }

    You can read more about how the User is connected to the rest of the auth system and how you can access the user data in the Accessing User Data section of the docs.

    3. Add the Routes and Pages

    Next, we need to define the routes and pages for the authentication pages.

    Add the following to the main.wasp file:

    main.wasp
    // ...
    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { Login } from "@src/pages/auth.jsx"
    }
    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { Signup } from "@src/pages/auth.jsx"
    }

    We'll define the React components for these pages in the src/pages/auth.tsx file below.

    4. Create the Client Pages

    info

    We are using Tailwind CSS to style the pages. Read more about how to add it here.

    Let's create a auth.tsx file in the src/pages folder and add the following to it:

    src/pages/auth.jsx
    import { LoginForm, SignupForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function Login() {
    return (
    <Layout>
    <LoginForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    Don't have an account yet? <Link to="/signup">go to signup</Link>.
    </span>
    </Layout>
    );
    }

    export function Signup() {
    return (
    <Layout>
    <SignupForm />
    <br />
    <span className="text-sm font-medium text-gray-900">
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </Layout>
    );
    }

    // A layout component to center the content
    export function Layout({ children }) {
    return (
    <div className="w-full h-full bg-white">
    <div className="min-w-full min-h-[75vh] flex items-center justify-center">
    <div className="w-full h-full max-w-sm p-5 bg-white">
    <div>{children}</div>
    </div>
    </div>
    </div>
    );
    }

    We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

    Conclusion

    That's it! We have set up username authentication in our app. 🎉

    Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the auth overview docs.

    Using multiple auth identities for a single user

    Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.

    Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.

    Customizing the Auth Flow

    The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

    Read more about the default username and password validation rules in the auth overview docs.

    If you require more control in your authentication flow, you can achieve that in the following ways:

    1. Create your UI and use signup and login actions.
    2. Create your custom sign-up action which uses the lower-level API, along with your custom code.

    1. Using the signup and login actions

    login()

    An action for logging in the user.

    It takes two arguments:

    • username: string required

      Username of the user logging in.

    • password: string required

      Password of the user logging in.

    You can use it like this:

    src/pages/auth.jsx
    import { login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory, Link } from 'react-router-dom'

    export function LoginPage() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await login(username, password)
    history.push('/')
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }
    note

    When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

    signup()

    An action for signing up the user. This action does not log in the user, you still need to call login().

    It takes one argument:

    • userFields: object required

      It has the following fields:

      • username: string required

      • password: string required

      info

      By default, Wasp will only save the username and password fields. If you want to add extra fields to your signup process, read about defining extra signup fields.

    You can use it like this:

    src/pages/auth.jsx
    import { signup, login } from 'wasp/client/auth'

    import { useState } from 'react'
    import { useHistory } from 'react-router-dom'
    import { Link } from 'react-router-dom'

    export function Signup() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState(null)
    const history = useHistory()

    async function handleSubmit(event) {
    event.preventDefault()
    try {
    await signup({
    username,
    password,
    })
    await login(username, password)
    history.push("/")
    } catch (error) {
    setError(error)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    {/* ... */}
    </form>
    );
    }

    2. Creating your custom sign-up action

    The code of your custom sign-up action can look like this:

    main.wasp
    // ...

    action customSignup {
    fn: import { signup } from "@src/auth/signup.js",
    }
    src/auth/signup.js
    import {
    ensurePasswordIsPresent,
    ensureValidPassword,
    ensureValidUsername,
    createProviderId,
    sanitizeAndSerializeProviderData,
    createUser,
    } from 'wasp/server/auth'

    export const signup = async (args, _context) => {
    ensureValidUsername(args)
    ensurePasswordIsPresent(args)
    ensureValidPassword(args)

    try {
    const providerId = createProviderId('username', args.username)
    const providerData = await sanitizeAndSerializeProviderData({
    hashedPassword: args.password,
    })

    await createUser(
    providerId,
    providerData,
    // Any additional data you want to store on the User entity
    {},
    )
    } catch (e) {
    return {
    success: false,
    message: e.message,
    }
    }

    // Your custom code after sign-up.
    // ...

    return {
    success: true,
    message: 'User created successfully',
    }
    }

    We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth. These are the same validators that Wasp uses internally for the default authentication flow.

    Username

    • ensureValidUsername(args)

      Checks if the username is valid and throws an error if it's not. Read more about the validation rules here.

    Password

    • ensurePasswordIsPresent(args)

      Checks if the password is present and throws an error if it's not.

    • ensureValidPassword(args)

      Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.

    Using Auth

    To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.

    When you receive the user object on the client or the server, you'll be able to access the user's username like this:

    const usernameIdentity = user.identities.username

    // Username that the user used to sign up, e.g. "fluffyllama"
    usernameIdentity.id

    Read more about accessing the user data in the Accessing User Data section of the docs.

    API Reference

    userEntity fields

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login"
    }
    }
    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    }

    The user entity needs to have the following fields:

    • id required

      It can be of any type, but it needs to be marked with @id

    You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields field if they need to be set during the sign-up process.

    Fields in the usernameAndPassword dict

    main.wasp
    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {
    userSignupFields: import { userSignupFields } from "@src/auth/email.js",
    },
    },
    onAuthFailedRedirectTo: "/login"
    }
    }
    // ...

    userSignupFields: ExtImport

    userSignupFields defines all the extra fields that need to be set on the User during the sign-up process. For example, if you have address and phone fields on your User entity, you can set them by defining the userSignupFields like this:

    src/auth.js
    import { defineUserSignupFields } from 'wasp/server/auth'

    export const userSignupFields = defineUserSignupFields({
    address: (data) => {
    if (!data.address) {
    throw new Error('Address is required')
    }
    return data.address
    }
    phone: (data) => data.phone,
    })
    Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields).
    - - + + \ No newline at end of file diff --git a/docs/contact.html b/docs/contact.html index af45cdad16..b63eec6c02 100644 --- a/docs/contact.html +++ b/docs/contact.html @@ -19,13 +19,13 @@ - - + + - - + + \ No newline at end of file diff --git a/docs/contributing.html b/docs/contributing.html index ef8938f072..6b06f6a454 100644 --- a/docs/contributing.html +++ b/docs/contributing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Contributing

    Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

    Some side notes to make your journey easier:

    1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

    2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

    3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

    Happy hacking!

    - - + + \ No newline at end of file diff --git a/docs/data-model/backends.html b/docs/data-model/backends.html index 7780526482..e85a15501c 100644 --- a/docs/data-model/backends.html +++ b/docs/data-model/backends.html @@ -19,8 +19,8 @@ - - + +
    @@ -34,7 +34,7 @@ Wasp defines DbSeedFn like this:

    type DbSeedFn = (prisma: PrismaClient) => Promise<void>

    Annotating the function devSeedSimple with this type tells TypeScript:

    • The seeding function's argument (prisma) is of type PrismaClient.
    • The seeding function's return value is Promise<void>.

    Running seed functions

    Run the command wasp db seed and Wasp will ask you which seed function you'd like to run (if you've defined more than one).

    Alternatively, run the command wasp db seed <seed-name> to choose a specific seed function right away, for example:

    wasp db seed devSeedSimple

    Check the API Reference for more details on these commands.

    tip

    You'll often want to call wasp db seed right after you run wasp db reset, as it makes sense to fill the database with initial data after clearing it.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    db: {
    seeds: [
    import devSeed from "@src/dbSeeds.js"
    ],
    }
    }

    app.db is a dictionary with the following fields (all fields are optional):

    • seeds: [ExtImport]

      Defines the seed functions you can use with the wasp db seed command to seed your database with initial data. Read the Seeding section for more details.

    CLI Commands for Seeding the Database

    Use one of the following commands to run the seed functions:

    • wasp db seed

      If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.

    • wasp db seed <seed-name>

      This command runs the seed function with the specified name. The name is the identifier used in its import expression in the app.db.seeds list. For example, to run the seed function devSeedSimple which was defined like this:

      main.wasp
      app MyApp {
      // ...
      db: {
      seeds: [
      // ...
      import { devSeedSimple } from "@src/dbSeeds.js",
      ]
      }
      }

      Use the following command:

      wasp db seed devSeedSimple
    - - + + \ No newline at end of file diff --git a/docs/data-model/crud.html b/docs/data-model/crud.html index a8b8825c2d..0f9c58076f 100644 --- a/docs/data-model/crud.html +++ b/docs/data-model/crud.html @@ -19,8 +19,8 @@ - - + +
    @@ -29,7 +29,7 @@ Read more about the default implementations here.

    Here's the src/tasks.ts file:

    src/tasks.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    Using the Generated CRUD Operations on the Client

    And let's use the generated operations in our client code:

    src/MainPage.jsx
    import { Tasks } from 'wasp/client/crud'
    import { useState } from 'react'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
    const createTask = Tasks.create.useAction()
    const [taskDescription, setTaskDescription] = useState('')

    function handleCreateTask() {
    createTask({ description: taskDescription, isDone: false })
    setTaskDescription('')
    }

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    return (
    <div
    style={{
    fontSize: '1.5rem',
    display: 'grid',
    placeContent: 'center',
    height: '100vh',
    }}
    >
    <div>
    <input
    value={taskDescription}
    onChange={(e) => setTaskDescription(e.target.value)}
    />
    <button onClick={handleCreateTask}>Create task</button>
    </div>
    <ul>
    {tasks.map((task) => (
    <li key={task.id}>{task.description}</li>
    ))}
    </ul>
    </div>
    )
    }

    And here are the login and signup pages, where we are using Wasp's Auth UI components:

    src/LoginPage.jsx
    import { LoginForm } from 'wasp/client/auth'
    import { Link } from 'react-router-dom'

    export function LoginPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <LoginForm />
    <div>
    <Link to="/signup">Create an account</Link>
    </div>
    </div>
    )
    }
    src/SignupPage.jsx
    import { SignupForm } from 'wasp/client/auth'

    export function SignupPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <SignupForm />
    </div>
    )
    }

    That's it. You can now run wasp start and see the app in action. ⚡️

    You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.

    Future of CRUD Operations in Wasp

    CRUD operations currently have a limited set of knowledge about the business logic they are implementing.

    • For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the create operation in the example above.
    • Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
    • Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.

    CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.

    We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on this GitHub issue to see how we are doing.

    API Reference

    CRUD declaration works on top of an existing entity declaration. We'll fully explore the API using two examples:

    1. A basic CRUD declaration that relies on default options.
    2. A more involved CRUD declaration that uses extra options and overrides.

    Declaring a CRUD With Default Options

    If we create CRUD operations for an entity named Task, like this:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    get: {},
    getAll: {},
    create: {},
    update: {},
    delete: {},
    },
    }

    Wasp will give you the following default implementations:

    get - returns one entity based on the id field

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.findUnique({ where: { id: args.id } })

    getAll - returns all entities

    // ...

    // If the operation is not public, Wasp checks if an authenticated user
    // is making the request.

    return Task.findMany()

    create - creates a new entity

    // ...
    return Task.create({ data: args.data })

    update - updates an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.update({ where: { id: args.id }, data: args.data })

    delete - deletes an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.delete({ where: { id: args.id } })
    Current Limitations

    In the default create and update implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.

    In the future, we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.

    For now, the solution is to provide an override function. You can override the default implementation by using the overrideFn option and implementing the validation logic yourself.

    Declaring a CRUD With All Available Options

    Here's an example of a more complex CRUD declaration:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // optional, defaults to false
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@src/tasks.js", // optional
    },
    update: {},
    },
    }

    The CRUD declaration features the following fields:

    • entity: Entity required

      The entity to which the CRUD operations will be applied.

    • operations: { [operationName]: CrudOperationOptions } required

      The operations to be generated. The key is the name of the operation, and the value is the operation configuration.

      • The possible values for operationName are:
        • getAll
        • get
        • create
        • update
        • delete
      • CrudOperationOptions can have the following fields:
        • isPublic: bool - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to false.
        • overrideFn: ExtImport - The import statement of the optional override implementation in Node.js.

    Defining the overrides

    Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:

    • args

      The arguments of the operation i.e. the data sent from the client.

    • context

      Context contains the user making the request and the entities object with the entity that's being operated on.

    For a usage example, check the example guide.

    Using the CRUD operations in client code

    On the client, you import the CRUD operations from wasp/client/crud by import the {crud name} object. For example, if you have a CRUD called Tasks, you would import the operations like this:

    SomePage.jsx
    import { Tasks } from 'wasp/client/crud'

    You can then access the operations like this:

    SomePage.jsx
    const { data } = Tasks.getAll.useQuery()
    const { data } = Tasks.get.useQuery({ id: 1 })
    const createAction = Tasks.create.useAction()
    const updateAction = Tasks.update.useAction()
    const deleteAction = Tasks.delete.useAction()

    All CRUD operations are implemented with Queries and Actions under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)


    Join our community on Discord, where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.

    - - + + \ No newline at end of file diff --git a/docs/data-model/entities.html b/docs/data-model/entities.html index 99ebf60d29..2d9dcc1949 100644 --- a/docs/data-model/entities.html +++ b/docs/data-model/entities.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. This means that you use the schema.prisma file to define your database models and relationships. Wasp understands the Prisma schema file and picks up all the models you define there. You can read more about this in the Prisma Schema File section of the docs.

    In your project, you'll find a schema.prisma file in the root directory:

    .
    ├── main.wasp
    ...
    ├── schema.prisma
    ├── src
    ├── tsconfig.json
    └── vite.config.ts

    Prisma uses the Prisma Schema Language, a simple definition language explicitly created for defining models. The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:

    Defining an Entity

    A Prisma model declaration in the schema.prisma file represents a Wasp Entity.

    Entity vs Model

    You might wonder why we distinguish between a Wasp Entity and a Prisma model if they're essentially the same thing right now.

    While defining a Prisma model is currently the only way to create an Entity in Wasp, the Entity concept is a higher-level abstraction. We plan to expand on Entities in the future, both in terms of how you can define them and what you can do with them.

    So, think of an Entity as a Wasp concept and a model as a Prisma concept. For now, all Prisma models are Entities and vice versa, but this relationship might evolve as Wasp grows.

    Here's how you could define an Entity that represents a Task:

    schema.prisma
    model Task {
    id String @id @default(uuid())
    description String
    isDone Boolean @default(false)
    }

    The above Prisma model definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the tasks table has three columns):

    • id - A string value serving as a primary key. The database automatically generates it by generating a random unique ID.
    • description - A string value for storing the task's description.
    • isDone - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to false by default.

    Working with Entities

    Let's see how you can define and work with Wasp Entities:

    1. Create/update some Entities in the schema.prisma file.
    2. Run wasp db migrate-dev. This command syncs the database model with the Entity definitions the schema.prisma file. It does this by creating migration scripts.
    3. Migration scripts are automatically placed in the migrations/ folder. Make sure to commit this folder into version control.
    4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about operations).

    Using Entities in Operations

    Most of the time, you will be working with Entities within the context of Operations (Queries & Actions). We'll see how that's done on the next page.

    Using Entities directly

    If you need more control, you can directly interact with Entities by importing and using the Prisma Client. We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.

    You can only use the Prisma Client in your Wasp server code. You can import it like this:

    import { prisma } from 'wasp/server'

    prisma.task.create({
    description: "Read the Entities doc",
    isDone: true // almost :)
    })

    Next steps

    Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!

    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/actions.html b/docs/data-model/operations/actions.html index 7c64c3a7aa..088ade6976 100644 --- a/docs/data-model/operations/actions.html +++ b/docs/data-model/operations/actions.html @@ -19,8 +19,8 @@ - - + +
    @@ -47,7 +47,7 @@ Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

    1. args (type depends on the Action)

      An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

    2. context (type depends on the Action)

      An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

    Example

    The following Action:

    action createFoo {
    fn: import { createFoo } from "@src/actions.js"
    entities: [Foo]
    }

    Expects to find a named export createfoo from the file src/actions.js

    actions.js
    export const createFoo = (args, context) => {
    // implementation
    }

    The useAction Hook and Optimistic Updates

    Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

    When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

    The useAction hook accepts two arguments:

    • actionFn required

      The Wasp Action (the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

    • actionOptions

      An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

      • optimisticUpdates

        An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

        • getQuerySpecifier required

        A function returning the Query specifier (a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (you can use the properties of the added/changed item to address the Query).

        • updateQuery required

        The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

        • item - The argument you pass into the decorated Action.
        • oldData - The currently cached value for the Query identified by the specifier.
    caution

    The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

    Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

    Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

    Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

    src/pages/Task.jsx
    import React from 'react'
    import {
    useQuery,
    useAction,
    getTask,
    markTaskAsDone,
    } from 'wasp/client/operations'

    const TaskPage = ({ id }) => {
    const { data: task } = useQuery(getTask, { id })
    const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
    optimisticUpdates: [
    {
    getQuerySpecifier: ({ id }) => [getTask, { id }],
    updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
    },
    ],
    })

    if (!task) {
    return <h1>"Loading"</h1>
    }

    const { description, isDone } = task
    return (
    <div>
    <p>
    <strong>Description: </strong>
    {description}
    </p>
    <p>
    <strong>Is done: </strong>
    {isDone ? 'Yes' : 'No'}
    </p>
    {isDone || (
    <button onClick={() => markTaskAsDoneOptimistically({ id })}>
    Mark as done.
    </button>
    )}
    </div>
    )
    }

    export default TaskPage

    Advanced usage

    The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

    Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

    If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

    import { getTasks } from 'wasp/client/operations'

    const queryKey = getTasks.queryCacheKey
    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/overview.html b/docs/data-model/operations/overview.html index 9c74476d37..2a016e1633 100644 --- a/docs/data-model/operations/overview.html +++ b/docs/data-model/operations/overview.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Overview

    While Entities enable you to define your app's data model and relationships, Operations are all about working with this data.

    There are two kinds of Operations: Queries and Actions. As their names suggest, Queries are meant for reading data, and Actions are meant for changing it (either by updating existing entries or creating new ones).

    Keep reading to find out all there is to know about Operations in Wasp.

    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/queries.html b/docs/data-model/operations/queries.html index 3adccb322a..1fcb0651c6 100644 --- a/docs/data-model/operations/queries.html +++ b/docs/data-model/operations/queries.html @@ -19,8 +19,8 @@ - - + +
    @@ -50,7 +50,7 @@ behavior for this particular Query. If you want to change the global defaults, you can do so in the client setup function.

    For an example of usage, check this section.

    - - + + \ No newline at end of file diff --git a/docs/data-model/prisma-file.html b/docs/data-model/prisma-file.html index 9a8b80b15a..4856ddae27 100644 --- a/docs/data-model/prisma-file.html +++ b/docs/data-model/prisma-file.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Prisma Schema File

    Wasp uses Prisma to interact with the database. Prisma is a "Next-generation Node.js and TypeScript ORM" that provides a type-safe API for working with your database.

    With Prisma, you define your application's data model in a schema.prisma file. Read more about how Wasp Entities relate to Prisma models on the Entities page.

    In Wasp, the schema.prisma file is located in your project's root directory:

    .
    ├── main.wasp
    ...
    ├── schema.prisma
    ├── src
    ├── tsconfig.json
    └── vite.config.ts

    Wasp uses the schema.prisma file to understand your app's data model and generate the necessary code to interact with the database.

    Wasp file and Prisma schema file

    Let's see how Wasp and Prisma files work together to define your application.

    Here's an example schema.prisma file where we defined some database options and two models (User and Task) with a one-to-many relationship:

    schema.prisma
    datasource db {
    provider = "postgresql"
    url = env("DATABASE_URL")
    }

    generator client {
    provider = "prisma-client-js"
    }

    model User {
    id Int @id @default(autoincrement())
    tasks Task[]
    }

    model Task {
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User @relation(fields: [userId], references: [id])
    userId Int
    }

    Wasp reads this schema.prisma file and extracts the info about your database models and database config.

    The datasource block defines which database you want to use (PostgreSQL in this case) and some other options.

    The generator block defines how to generate the Prisma Client code that you can use in your application to interact with the database.

    Relationship between Wasp file and Prisma file
    Relationship between Wasp file and Prisma file

    Finally, Prisma models become Wasp Entities which can be then used in the main.wasp file:

    main.wasp
    app myApp {
    wasp: {
    version: "^0.14.0"
    },
    title: "My App",
    }

    ...

    // Using Wasp Entities in the Wasp file

    query getTasks {
    fn: import { getTasks } from "@src/queries",
    entities: [Task]
    }

    job myJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@src/workers/bar"
    },
    entities: [Task],
    }

    api fooBar {
    fn: import { fooBar } from "@src/apis",
    entities: [Task],
    httpRoute: (GET, "/foo/bar/:email")
    }

    In the implementation of the getTasks query, Task is a Wasp Entity that corresponds to the Task model defined in the schema.prisma file.

    The same goes for the myJob job and fooBar API, where Task is used as an Entity.

    To learn more about the relationship between Wasp Entities and Prisma models, check out the Entities page.

    Wasp-specific Prisma configuration

    Wasp mostly lets you use the Prisma schema file as you would in any other JS/TS project. However, there are some Wasp-specific rules you need to follow.

    The datasource block

    schema.prisma
    datasource db {
    provider = "postgresql"
    url = env("DATABASE_URL")
    }

    Wasp takes the datasource you write and use it as-is.

    There are some rules you need to follow:

    • You can only use "postgresql" or "sqlite" as the provider because Wasp only supports PostgreSQL and SQLite databases for now.
    • You must set the url field to env("DATABASE_URL") so that Wasp can work properly with your database.

    The generator blocks

    schema.prisma
    generator client {
    provider = "prisma-client-js"
    }

    Wasp requires that there is a generator block with provider = "prisma-client-js" in the schema.prisma file.

    You can add additional generators if you need them in your project.

    The model blocks

    schema.prisma
    model User {
    id Int @id @default(autoincrement())
    tasks Task[]
    }

    model Task {
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User @relation(fields: [userId], references: [id])
    userId Int
    }

    You can define your models in any way you like, if it's valid Prisma schema code, it will work with Wasp.

    Triple slash comments

    Wasp doesn't yet fully support /// comment syntax in the schema.prisma file. We are tracking it here, let us know if this is something you need.

    Prisma preview features

    Prisma is still in active development and some of its features are not yet stable. To enable various preview features in Prisma, you need to add the previewFeatures field to the generator block in the schema.prisma file.

    For example, one useful Prisma preview feature is PostgreSQL extensions support, which allows you to use PostgreSQL extensions like pg_vector or pg_trgm in your database schema:

    schema.prisma
    datasource db {
    provider = "postgresql"
    url = env("DATABASE_URL")
    extensions = [pgvector(map: "vector")]
    }

    generator client {
    provider = "prisma-client-js"
    previewFeatures = ["postgresqlExtensions"]
    }

    // ...

    Read more about preview features in the Prisma docs here or about using PostgreSQL extensions here.

    - - + + \ No newline at end of file diff --git a/docs/editor-setup.html b/docs/editor-setup.html index 433b33736c..3ee6050afb 100644 --- a/docs/editor-setup.html +++ b/docs/editor-setup.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • the Prisma extension for .prisma files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    LSP Problems

    If you are using TypeScript, your editor may sometimes report type and import errors even while wasp start is running.

    This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting "TypeScript: Restart TS Server." Open the command pallete with:

    • Ctrl + Shift + P if you're on Windows or Linux.
    • Cmd + Shift + P if you're on a Mac.
    - - + + \ No newline at end of file diff --git a/docs/general/cli.html b/docs/general/cli.html index dc64f8c050..2b0fb8c0e9 100644 --- a/docs/general/cli.html +++ b/docs/general/cli.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    new:ai <app-name> <app-description> [<config-json>]
    Uses AI to create a new Wasp project just based on the app name and the description.
    You can do the same thing with `wasp new` interactively.
    Run `wasp new:ai` for more info.

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code, all cached artifacts, and the node_modules dir.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about the current Wasp project.
    test Executes tests in your project.
    studio (experimental) GUI for inspecting your Wasp app.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      Simple starter template with a single page.
      [2] todo-ts
      Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
      [3] saas
      Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
      [4] embeddings
      Comes with code for generating vector embeddings and performing vector similarity search.
      [5] ai-generated
      🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
      ▸ 1

      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the "basic" template... -------------------------

      Created new Wasp app in ./MyFirstProject directory!

      To run your new app, do:
      cd MyFirstProject
      wasp db start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      🐝 --- Deleting the .wasp/ directory... -------------------------------------------

      ✅ --- Deleted the .wasp/ directory. ----------------------------------------------

      🐝 --- Deleting the node_modules/ directory... ------------------------------------

      ✅ --- Deleted the node_modules/ directory. ---------------------------------------
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    • wasp studio shows you an graphical overview of your application in a graph: pages, queries, actions, data model etc.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    using prisma CLI directly

    Although Wasp uses the schema.prisma file to define the database schema, you must not use the prisma command directly. Instead, use the wasp db commands.

    Wasp adds some additional functionality on top of Prisma, and using prisma commands directly can lead to unexpected behavior e.g. missing auth models, incorrect database setup, etc.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.14.0

      If you wish to install/switch to the latest version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s

      If you want specific x.y.z version of Wasp, do:
      curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v x.y.z

      Check https://github.com/wasp-lang/wasp/releases for the list of valid versions, including the latest one.
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    - - + + \ No newline at end of file diff --git a/docs/general/language.html b/docs/general/language.html index 766838d9e8..1c5642df76 100644 --- a/docs/general/language.html +++ b/docs/general/language.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Wasp Language (.wasp)

    Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

    It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

    It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

    Declarations

    The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

    app MyApp {
    title: "My app"
    }

    route RootRoute { path: "/", to: DashboardPage }

    page DashboardPage {
    component: import { DashboardPage } from "@src/Dashboard.jsx"
    }

    In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

    Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

    • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
    • <declaration_name> is an identifier chosen by you to name this specific declaration
    • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

    So, for app declaration above, we have:

    • declaration type app
    • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
    • declaration body { title: "My app" }, which is a dictionary with field title that has string value. Type of this dictionary is in line with the declaration body type of the app declaration type. If we provided something else, e.g. changed title to little, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for app.

    Each declaration has a meaning behind it that describes how your web app should behave and function.

    All the other types in Wasp language (primitive types (string, number), composite types (dict, list), enum types (DbSystem), ...) are used to define the declaration bodies.

    Complete List of Wasp Types

    Wasp's type system can be divided into two main categories of types: fundamental types and domain types.

    While fundamental types are here to be basic building blocks of a language and are very similar to what you would see in other popular languages, domain types are what make Wasp special, as they model the concepts of a web app like page, route and similar.

    • Fundamental types (source of truth)
      • Primitive types
        • string ("foo", "they said: \"hi\"")
        • bool (true, false)
        • number (12, 14.5)
        • declaration reference (name of existing declaration: TaskPage, updateTask)
        • ExtImport (external import) (import Foo from "@src/bar.js", import { Smth } from "@src/a/b.js")
          • The path has to start with "@src". The rest is relative to the src directory.
          • Import has to be a default import import Foo or a single named import import { Foo }.
        • json ({=json { a: 5, b: ["hi"] } json=})
      • Composite types
        • dict (dictionary) ({ a: 5, b: "foo" })
        • list ([1, 2, 3])
        • tuple ((1, "bar"), (2, 4, true))
          • Tuples can be of size 2, 3 and 4.
    • Domain types (source of truth)
      • Declaration types
        • action
        • api
        • apiNamespace
        • app
        • job
        • page
        • query
        • route
        • crud
      • Enum types
        • DbSystem
        • HttpMethod
        • JobExecutor
        • EmailProvider
      • Models from the schema.prisma file
        • You can reference models defined in the schema.prisma file in your Wasp file by using the model name e.g. Task.

    You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.

    - - + + \ No newline at end of file diff --git a/docs/general/typescript.html b/docs/general/typescript.html index c080aa62f6..3af65db747 100644 --- a/docs/general/typescript.html +++ b/docs/general/typescript.html @@ -19,8 +19,8 @@ - - + +
    @@ -33,7 +33,7 @@ support when implementing the Query. Thanks to this type, the compiler knows:

    • The type of the context object.
    • The type of args.
    • The Query's return type.

    And gives you Intellisense and type-checking. Read more about this feature here.

    You don't need to change anything inside the .wasp file.

    Migrating the rest of the project

    You can migrate your project gradually - on a file-by-file basis.

    When you want to migrate a file, follow the procedure outlined above:

    1. Change the file's extension.
    2. Fix the type errors.
    3. Read the Wasp docs and decide which TypeScript features you want to use.
    LSP Problems

    If you are using TypeScript, your editor may sometimes report type and import errors even while wasp start is running.

    This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting "TypeScript: Restart TS Server." Open the command pallete with:

    • Ctrl + Shift + P if you're on Windows or Linux.
    • Cmd + Shift + P if you're on a Mac.
    - - + + \ No newline at end of file diff --git a/docs/migrate-from-0-11-to-0-12.html b/docs/migrate-from-0-11-to-0-12.html index 859eebb6a7..bfc3148cd5 100644 --- a/docs/migrate-from-0-11-to-0-12.html +++ b/docs/migrate-from-0-11-to-0-12.html @@ -19,8 +19,8 @@ - - + +
    @@ -56,7 +56,7 @@ src/server), you are now free to reorganize your project however you think is best, as long as you keep all the source files in the src/ directory.

    This section is optional, but if you didn't like the server/client separation, now's the perfect time to change it.

    For example, if your src dir looked like this:

    src

    ├── client
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── MainPage.tsx
    │   ├── Register.tsx
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   ├── Task.tsx
    │   └── User.tsx
    ├── server
    │   ├── taskActions.ts
    │   ├── taskQueries.ts
    │   ├── userActions.ts
    │   └── userQueries.ts
    └── shared
    └── utils.ts

    you can now change it to a feature-based structure (which we recommend for any project that is not very small):

    src

    ├── task
    │   ├── actions.ts -- former taskActions.ts
    │   ├── queries.ts -- former taskQueries.ts
    │   ├── Task.css
    │   ├── TaskLisk.tsx
    │   └── Task.tsx
    ├── user
    │   ├── actions.ts -- former userActions.ts
    │   ├── Dashboard.tsx
    │   ├── Login.tsx
    │   ├── queries.ts -- former userQueries.ts
    │   ├── Register.tsx
    │   └── User.tsx
    ├── MainPage.tsx
    └── utils.ts

    Appendix

    Example Data Migration Functions

    The migration functions provided below are written with the typical use cases in mind and you can use them as-is. If your setup requires additional logic, you can use them as a good starting point and modify them to your needs.

    Note that all of the functions below are written to be idempotent, meaning that running a function multiple times can't hurt. This allows executing a function again in case only a part of the previous execution succeeded and also means that accidentally running it one time too much won't have any negative effects. We recommend you keep your data migration functions idempotent.

    Username & Password

    To successfully migrate the users using the Username & Password auth method, you will need to do two things:

    1. Migrate the user data

      Username & Password data migration function
      main.wasp
      api migrateUsernameAndPassword {
      httpRoute: (GET, "/migrate-username-and-password"),
      fn: import { migrateUsernameAndPasswordHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type UsernameProviderData } from "wasp/server/auth";
      import { MigrateUsernameAndPassword } from "wasp/server/api";

      export const migrateUsernameAndPasswordHandler: MigrateUsernameAndPassword =
      async (_req, res) => {
      const result = await migrateUsernameAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateUsernameAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.username || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using username auth) with id:", user.id);
      continue;
      }

      const providerData: UsernameProviderData = {
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "username";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.username.toLowerCase(),
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Provide a way for users to migrate their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to migrate their password after the migration, as the old password will no longer work.

      Since the only way users using username and password as a login method can verify their identity is by providing both their username and password (there is no email or any other info, unless you asked for it and stored it explicitly), we need to provide them a way to exchange their old password for a new password. One way to handle this is to inform them about the need to migrate their password (on the login page) and provide a custom page to migrate the password.

    Steps to create a custom page for migrating the password
    1. You will need to install the secure-password and sodium-native packages to use the old hashing algorithm:

      npm install secure-password@4.0.0 sodium-native@3.3.0 --save-exact

      Make sure to save the exact versions of the packages.

    2. Then you'll need to create a new page in your app where users can migrate their password. You can use the following code as a starting point:

    main.wasp
    route MigratePasswordRoute { path: "/migrate-password", to: MigratePassword }
    page MigratePassword {
    component: import { MigratePasswordPage } from "@src/pages/MigratePassword"
    }
    src/pages/MigratePassword.jsx
    import {
    FormItemGroup,
    FormLabel,
    FormInput,
    FormError,
    } from "wasp/client/auth";
    import { useForm } from "react-hook-form";
    import { migratePassword } from "wasp/client/operations";
    import { useState } from "react";

    export function MigratePasswordPage() {
    const [successMessage, setSuccessMessage] = useState(null);
    const [errorMessage, setErrorMessage] = useState(null);
    const form = useForm();

    const onSubmit = form.handleSubmit(async (data) => {
    try {
    const result = await migratePassword(data);
    setSuccessMessage(result.message);
    } catch (e) {
    console.error(e);
    if (e instanceof Error) {
    setErrorMessage(e.message);
    }
    }
    });

    return (
    <div style={{
    maxWidth: "400px",
    margin: "auto",
    }}>
    <h1>Migrate your password</h1>
    <p>
    If you have an account on the old version of the website, you can
    migrate your password to the new version.
    </p>
    {successMessage && <div>{successMessage}</div>}
    {errorMessage && <FormError>{errorMessage}</FormError>}
    <form onSubmit={onSubmit}>
    <FormItemGroup>
    <FormLabel>Username</FormLabel>
    <FormInput
    {...form.register("username", {
    required: "Username is required",
    })}
    />
    <FormError>{form.formState.errors.username?.message}</FormError>
    </FormItemGroup>
    <FormItemGroup>
    <FormLabel>Password</FormLabel>
    <FormInput
    {...form.register("password", {
    required: "Password is required",
    })}
    type="password"
    />
    <FormError>{form.formState.errors.password?.message}</FormError>
    </FormItemGroup>
    <button type="submit">Migrate password</button>
    </form>
    </div>
    );
    }
    1. Finally, you will need to create a new operation in your app to handle the password migration. You can use the following code as a starting point:
    main.wasp
    action migratePassword {
    fn: import { migratePassword } from "@src/auth",
    entities: []
    }
    src/auth.js
    import SecurePassword from "secure-password";
    import { HttpError } from "wasp/server";
    import {
    createProviderId,
    deserializeAndSanitizeProviderData,
    findAuthIdentity,
    updateAuthIdentityProviderData,
    } from "wasp/server/auth";

    export const migratePassword = async ({ password, username }, _context) => {
    const providerId = createProviderId("username", username);
    const authIdentity = await findAuthIdentity(providerId);

    if (!authIdentity) {
    throw new HttpError(400, "Something went wrong");
    }

    const providerData = deserializeAndSanitizeProviderData(
    authIdentity.providerData
    );

    try {
    const SP = new SecurePassword();

    // This will verify the password using the old algorithm
    const result = await SP.verify(
    Buffer.from(password),
    Buffer.from(providerData.hashedPassword, "base64")
    );

    if (result !== SecurePassword.VALID) {
    throw new HttpError(400, "Something went wrong");
    }

    // This will hash the password using the new algorithm and update the
    // provider data in the database.
    await updateAuthIdentityProviderData(providerId, providerData, {
    hashedPassword: password,
    });
    } catch (e) {
    throw new HttpError(400, "Something went wrong");
    }

    return {
    message: "Password migrated successfully.",
    };
    };

    Email

    To successfully migrate the users using the Email auth method, you will need to do two things:

    1. Migrate the user data

      Email data migration function
      main.wasp
      api migrateEmail {
      httpRoute: (GET, "/migrate-email"),
      fn: import { migrateEmailHandler } from "@src/migrateToNewAuth",
      entities: []
      }
      src/migrateToNewAuth.ts
      import { prisma } from "wasp/server";
      import { type ProviderName, type EmailProviderData } from "wasp/server/auth";
      import { MigrateEmail } from "wasp/server/api";

      export const migrateEmailHandler: MigrateEmail =
      async (_req, res) => {
      const result = await migrateEmailAuth();

      res.status(200).json({ message: "Migrated users to the new auth", result });
      };

      async function migrateEmailAuth(): Promise<{
      numUsersAlreadyMigrated: number;
      numUsersNotUsingThisAuthMethod: number;
      numUsersMigratedSuccessfully: number;
      }> {
      const users = await prisma.user.findMany({
      include: {
      auth: true,
      },
      });

      const result = {
      numUsersAlreadyMigrated: 0,
      numUsersNotUsingThisAuthMethod: 0,
      numUsersMigratedSuccessfully: 0,
      };

      for (const user of users) {
      if (user.auth) {
      result.numUsersAlreadyMigrated++;
      console.log("Skipping user (already migrated) with id:", user.id);
      continue;
      }

      if (!user.email || !user.password) {
      result.numUsersNotUsingThisAuthMethod++;
      console.log("Skipping user (not using email auth) with id:", user.id);
      continue;
      }

      const providerData: EmailProviderData = {
      isEmailVerified: user.isEmailVerified,
      emailVerificationSentAt:
      user.emailVerificationSentAt?.toISOString() ?? null,
      passwordResetSentAt: user.passwordResetSentAt?.toISOString() ?? null,
      hashedPassword: user.password,
      };
      const providerName: ProviderName = "email";

      await prisma.auth.create({
      data: {
      identities: {
      create: {
      providerName,
      providerUserId: user.email,
      providerData: JSON.stringify(providerData),
      },
      },
      user: {
      connect: {
      id: user.id,
      },
      },
      },
      });
      result.numUsersMigratedSuccessfully++;
      }

      return result;
      }
    2. Ask the users to reset their password

      There is a breaking change between the old and the new auth in the way the password is hashed. This means that users will need to reset their password after the migration, as the old password will no longer work.

      It would be best to notify your users about this change and put a notice on your login page to request a password reset.

    Google & GitHub

    Google & GitHub data migration functions
    main.wasp
    api migrateGoogle {
    httpRoute: (GET, "/migrate-google"),
    fn: import { migrateGoogleHandler } from "@src/migrateToNewAuth",
    entities: []
    }

    api migrateGithub {
    httpRoute: (GET, "/migrate-github"),
    fn: import { migrateGithubHandler } from "@src/migrateToNewAuth",
    entities: []
    }
    src/migrateToNewAuth.ts
    import { prisma } from "wasp/server";
    import { MigrateGoogle, MigrateGithub } from "wasp/server/api";

    export const migrateGoogleHandler: MigrateGoogle =
    async (_req, res) => {
    const result = await createSocialLoginMigration("google");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    export const migrateGithubHandler: MigrateGithub =
    async (_req, res) => {
    const result = await createSocialLoginMigration("github");

    res.status(200).json({ message: "Migrated users to the new auth", result });
    };

    async function createSocialLoginMigration(
    providerName: "google" | "github"
    ): Promise<{
    numUsersAlreadyMigrated: number;
    numUsersNotUsingThisAuthMethod: number;
    numUsersMigratedSuccessfully: number;
    }> {
    const users = await prisma.user.findMany({
    include: {
    auth: true,
    externalAuthAssociations: true,
    },
    });

    const result = {
    numUsersAlreadyMigrated: 0,
    numUsersNotUsingThisAuthMethod: 0,
    numUsersMigratedSuccessfully: 0,
    };

    for (const user of users) {
    if (user.auth) {
    result.numUsersAlreadyMigrated++;
    console.log("Skipping user (already migrated) with id:", user.id);
    continue;
    }

    const provider = user.externalAuthAssociations.find(
    (provider) => provider.provider === providerName
    );

    if (!provider) {
    result.numUsersNotUsingThisAuthMethod++;
    console.log(`Skipping user (not using ${providerName} auth) with id:`, user.id);
    continue;
    }

    await prisma.auth.create({
    data: {
    identities: {
    create: {
    providerName,
    providerUserId: provider.providerId,
    providerData: JSON.stringify({}),
    },
    },
    user: {
    connect: {
    id: user.id,
    },
    },
    },
    });
    result.numUsersMigratedSuccessfully++;
    }

    return result;
    }
    - - + + \ No newline at end of file diff --git a/docs/migrate-from-0-12-to-0-13.html b/docs/migrate-from-0-12-to-0-13.html index 6c911ea7a5..ca6800ab77 100644 --- a/docs/migrate-from-0-12-to-0-13.html +++ b/docs/migrate-from-0-12-to-0-13.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Migration from 0.12.X to 0.13.X

    Are you on 0.11.X or earlier?

    This guide only covers the migration from 0.12.X to 0.13.X. If you are migrating from 0.11.X or earlier, please read the migration guide from 0.11.X to 0.12.X first.

    Make sure to read the migration guide from 0.13.X to 0.14.X after you finish this one.

    What's new in 0.13.0?

    OAuth providers got an overhaul

    Wasp 0.13.0 switches away from using Passport for our OAuth providers in favor of Arctic from the Lucia ecosystem. This change simplifies the codebase and makes it easier to add new OAuth providers in the future.

    We added Keycloak as an OAuth provider

    Wasp now supports using Keycloak as an OAuth provider.

    How to migrate?

    Migrate your OAuth setup

    We had to make some breaking changes to upgrade the OAuth setup to the new Arctic lib.

    Follow the steps below to migrate:

    1. Define the WASP_SERVER_URL server env variable

      In 0.13.0 Wasp introduces a new server env variable WASP_SERVER_URL that you need to define. This is the URL of your Wasp server and it's used to generate the redirect URL for the OAuth providers.

      Server env variables
      WASP_SERVER_URL=https://your-wasp-server-url.com

      In development, Wasp sets the WASP_SERVER_URL to http://localhost:3001 by default.

      Migrating a deployed app

      If you are migrating a deployed app, you will need to define the WASP_SERVER_URL server env variable in your deployment environment.

      Read more about setting env variables in production here.

    2. Update the redirect URLs for the OAuth providers

      The redirect URL for the OAuth providers has changed. You will need to update the redirect URL for the OAuth providers in the provider's dashboard.

      {clientUrl}/auth/login/{provider}

      Check the new redirect URLs for Google and GitHub in Wasp's docs.

    3. Update the configFn for the OAuth providers

      If you didn't use the configFn option, you can skip this step.

      If you used the configFn to configure the scope for the OAuth providers, you will need to rename the scope property to scopes.

      Also, the object returned from configFn no longer needs to include the Client ID and the Client Secret. You can remove them from the object that configFn returns.

      google.ts
      export function getConfig() {
      return {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      scope: ['profile', 'email'],
      }
      }
    4. Update the userSignupFields fields to use the new profile format

      If you didn't use the userSignupFields option, you can skip this step.

      The data format for the profile that you receive from the OAuth providers has changed. You will need to update your code to reflect this change.

      google.ts
      import { defineUserSignupFields } from 'wasp/server/auth'

      export const userSignupFields = defineUserSignupFields({
      displayName: (data: any) => data.profile.displayName,
      })

      Wasp now directly forwards what it receives from the OAuth providers. You can check the data format for Google and GitHub in Wasp's docs.

    That's it!

    You should now be able to run your app with the new Wasp 0.13.0.

    - - + + \ No newline at end of file diff --git a/docs/migrate-from-0-13-to-0-14.html b/docs/migrate-from-0-13-to-0-14.html index 997bc81991..646d8bb021 100644 --- a/docs/migrate-from-0-13-to-0-14.html +++ b/docs/migrate-from-0-13-to-0-14.html @@ -19,8 +19,8 @@ - - + +
    @@ -30,7 +30,7 @@ below.

    If you have made changes to your tsconfig.json file, we recommend taking the new version of the file and reapplying them.

    Here's the new version of the tsconfig.json file:

    tsconfig.json
    // =============================== IMPORTANT =================================
    //
    // This file is only used for Wasp IDE support. You can change it to configure
    // your IDE checks, but none of these options will affect the TypeScript
    // compiler. Proper TS compiler configuration in Wasp is coming soon :)
    {
    "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    // We're bundling all code in the end so this is the most appropriate option,
    // it's also important for autocomplete to work properly.
    "moduleResolution": "bundler",
    // JSX support
    "jsx": "preserve",
    "strict": true,
    // Allow default imports.
    "esModuleInterop": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "typeRoots": [
    // This is needed to properly support Vitest testing with jest-dom matchers.
    // Types for jest-dom are not recognized automatically and Typescript complains
    // about missing types e.g. when using `toBeInTheDocument` and other matchers.
    "node_modules/@testing-library",
    // Specifying type roots overrides the default behavior of looking at the
    // node_modules/@types folder so we had to list it explicitly.
    // Source 1: https://www.typescriptlang.org/tsconfig#typeRoots
    // Source 2: https://github.com/testing-library/jest-dom/issues/546#issuecomment-1889884843
    "node_modules/@types"
    ],
    // Since this TS config is used only for IDE support and not for
    // compilation, the following directory doesn't exist. We need to specify
    // it to prevent this error:
    // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file
    "outDir": ".wasp/phantom"
    }
    }

    Migrate to the new schema.prisma file

    To use the new schema.prisma file, you need to move your entities from the .wasp file to the schema.prisma file.

    1. Create a new schema.prisma file

    Create a new file named schema.prisma in the root of your project:

    .
    ├── main.wasp
    ...
    ├── schema.prisma
    ├── src
    ├── tsconfig.json
    └── vite.config.ts

    2. Add the datasource block to the schema.prisma file

    This block specifies the database type and connection URL:

    schema.prisma
    datasource db {
    provider = "sqlite"
    url = env("DATABASE_URL")
    }
    • The provider should be either "postgresql" or "sqlite".

    • The url must be set to env("DATABASE_URL") so that Wasp can inject the database URL from the environment variables.

    3. Add the generator block to the schema.prisma file

    This block specifies the Prisma Client generator Wasp uses:

    schema.prisma
    datasource db {
    provider = "sqlite"
    url = env("DATABASE_URL")
    }

    generator client {
    provider = "prisma-client-js"
    }
    • The provider should be set to "prisma-client-js".

    4. Move your entities to the schema.prisma file

    Move the entities from the .wasp file to the schema.prisma file:

    schema.prisma
    datasource db {
    provider = "sqlite"
    url = env("DATABASE_URL")
    }

    generator client {
    provider = "prisma-client-js"
    }

    // There are some example entities, you should move your entities here
    model User {
    id Int @id @default(autoincrement())
    tasks Task[]
    }

    model Task {
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    }

    When moving the entities over, you'll need to change entity to model and remove the =psl and psl= tags.

    If you had the following in the .wasp file:

    main.wasp
    entity Task {=psl
    // Stays the same
    psl=}

    ... it would look like this in the schema.prisma file:

    schema.prisma
    model Task {
    // Stays the same
    }

    5. Remove app.db.system field from the Wasp file

    We now configure the DB system in the schema.prisma file, so there is no need for that field in the Wasp file.

    main.wasp
    app MyApp {
    // ...
    db: {
    system: PostgreSQL,
    }
    }

    6. Migrate Prisma preview features config to the schema.prisma file

    If you didn't use any Prisma preview features, you can skip this step.

    If you had the following in the .wasp file:

    main.wasp
    app MyApp {
    // ...
    db: {
    prisma: {
    clientPreviewFeatures: ["postgresqlExtensions"]
    dbExtensions: [
    { name: "hstore", schema: "myHstoreSchema" },
    { name: "pg_trgm" },
    { name: "postgis", version: "2.1" },
    ]
    }
    }
    }

    ... it will become this:

    schema.prisma
    datasource db {
    provider = "postgresql"
    url = env("DATABASE_URL")
    extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
    }

    generator client {
    provider = "prisma-client-js"
    previewFeatures = ["postgresqlExtensions"]
    }

    All that's left to do is migrate the database.

    To avoid type errors, it's best to take care of database migrations after you've migrated the rest of the code. So, just keep reading, and we will remind you to migrate the database as the last step of the migration guide.

    Read more about the Prisma Schema File and how Wasp uses it to generate the database schema and Prisma client.

    Migrate how you access user auth fields

    We had to make a couple of breaking changes to reach the new simpler API.

    Follow the steps below to migrate:

    1. Replace the getUsername helper with user.identities.username.id

      If you didn't use the getUsername helper in your code, you can skip this step.

      This helper changed and it no longer works with the user you receive as a prop on a page or through the context. You'll need to replace it with user.identities.username.id.

      src/MainPage.tsx
      import { getUsername, AuthUser } from 'wasp/auth'

      const MainPage = ({ user }: { user: AuthUser }) => {
      const username = getUsername(user)
      // ...
      }
      src/tasks.ts
      import { getUsername } from 'wasp/auth'

      export const createTask: CreateTask<...> = async (args, context) => {
      const username = getUsername(context.user)
      // ...
      }
    2. Replace the getEmail helper with user.identities.email.id

      If you didn't use the getEmail helper in your code, you can skip this step.

      This helper changed and it no longer works with the user you receive as a prop on a page or through the context. You'll need to replace it with user.identities.email.id.

      src/MainPage.tsx
      import { getEmail, AuthUser } from 'wasp/auth'

      const MainPage = ({ user }: { user: AuthUser }) => {
      const email = getEmail(user)
      // ...
      }
      src/tasks.ts
      import { getEmail } from 'wasp/auth'

      export const createTask: CreateTask<...> = async (args, context) => {
      const email = getEmail(context.user)
      // ...
      }
    3. Replace accessing providerData with user.identities.<provider>.<value>

      If you didn't use any data from the providerData object, you can skip this step.

      Replace <provider> with the provider name (for example username, email, google, github, etc.) and <value> with the field you want to access (for example isEmailVerified).

      src/MainPage.tsx
      import { findUserIdentity, AuthUser } from 'wasp/auth'

      function getProviderData(user: AuthUser) {
      const emailIdentity = findUserIdentity(user, 'email')
      // We needed this before check for proper type support
      return emailIdentity && 'isEmailVerified' in emailIdentity.providerData
      ? emailIdentity.providerData
      : null
      }

      const MainPage = ({ user }: { user: AuthUser }) => {
      const providerData = getProviderData(user)
      const isEmailVerified = providerData ? providerData.isEmailVerified : null
      // ...
      }
    4. Use getFirstProviderUserId directly on the user object

      If you didn't use getFirstProviderUserId in your code, you can skip this step.

      You should replace getFirstProviderUserId(user) with user.getFirstProviderUserId().

      src/MainPage.tsx
      import { getFirstProviderUserId, AuthUser } from 'wasp/auth'

      const MainPage = ({ user }: { user: AuthUser }) => {
      const userId = getFirstProviderUserId(user)
      // ...
      }
      src/tasks.ts
      import { getFirstProviderUserId } from 'wasp/auth'

      export const createTask: CreateTask<...> = async (args, context) => {
      const userId = getFirstProviderUserId(context.user)
      // ...
      }
    5. Replace findUserIdentity with checks on user.identities.<provider>

      If you didn't use findUserIdentity in your code, you can skip this step.

      Instead of using findUserIdentity to get the identity object, you can directly check if the identity exists on the identities object.

      src/MainPage.tsx
      import { findUserIdentity, AuthUser } from 'wasp/auth'

      const MainPage = ({ user }: { user: AuthUser }) => {
      const usernameIdentity = findUserIdentity(user, 'username')
      if (usernameIdentity) {
      // ...
      }
      }
      src/tasks.ts
      import { findUserIdentity } from 'wasp/auth'

      export const createTask: CreateTask<...> = async (args, context) => {
      const usernameIdentity = findUserIdentity(context.user, 'username')
      if (usernameIdentity) {
      // ...
      }
      }

    Migrate the database

    Finally, you can Run the Wasp CLI to regenerate the new Prisma client:

    wasp db migrate-dev

    This command generates the Prisma client based on the schema.prisma file.

    Read more about the Prisma Schema File and how Wasp uses it to generate the database schema and Prisma client.

    That's it!

    You should now be able to run your app with the new Wasp 0.14.0. We recommend reading through the updated Accessing User Data section to get a better understanding of the new API.

    - - + + \ No newline at end of file diff --git a/docs/project/client-config.html b/docs/project/client-config.html index baa6737ade..151a304cae 100644 --- a/docs/project/client-config.html +++ b/docs/project/client-config.html @@ -19,8 +19,8 @@ - - + +
    @@ -35,7 +35,7 @@ renders a custom layout:

    src/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return (
    <Provider store={store}>
    <Layout>{children}</Layout>
    </Provider>
    )
    }

    function Layout({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }
  • setupFn: ExtImport

    You can use this function to perform any custom setup (e.g., setting up client-side periodic jobs).

    src/myClientSetupCode.js
    export default async function mySetupFunction() {
    // Run some code
    }
  • baseDir: String

    If you need to serve the client from a subdirectory, you can use the baseDir option.

    If you set baseDir to /my-app for example, that will make Wasp set the basename prop of the Router to /my-app. It will also set the base option of the Vite config to /my-app.

    This means that if you serve your app from https://example.com/my-app, the router will work correctly, and all the assets will be served from https://example.com/my-app.

    Setting the correct env variable

    If you set the baseDir option, make sure that the WASP_WEB_CLIENT_URL env variable also includes that base directory.

    For example, if you are serving your app from https://example.com/my-app, the WASP_WEB_CLIENT_URL should be also set to https://example.com/my-app, and not just https://example.com.

  • - - + + \ No newline at end of file diff --git a/docs/project/css-frameworks.html b/docs/project/css-frameworks.html index dfecb47975..ae57dd30d4 100644 --- a/docs/project/css-frameworks.html +++ b/docs/project/css-frameworks.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── package.json
    ├── src
    │   ├── Main.css
    │   ├── MainPage.jsx
    │   ├── vite-env.d.ts
    │   └── waspLogo.png
    ├── public
    ├── tsconfig.json
    ├── vite.config.ts
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      const { resolveProjectPath } = require('wasp/dev')

      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [resolveProjectPath('./src/**/*.{js,jsx,ts,tsx}')],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, install them as npm development dependencies and add them to the plugins list in your tailwind.config.cjs file:

    npm install -D @tailwindcss/forms
    npm install -D @tailwindcss/typography

    and also

    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    - - + + \ No newline at end of file diff --git a/docs/project/custom-vite-config.html b/docs/project/custom-vite-config.html index 7e0bf79e6c..8c6772ef37 100644 --- a/docs/project/custom-vite-config.html +++ b/docs/project/custom-vite-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Custom Vite Config

    Wasp uses Vite to serve the client during development and bundling it for production. If you want to customize the Vite config, you can do that by editing the vite.config.ts file in your project root directory.

    Wasp will use your config and merge it with the default Wasp's Vite config.

    Vite config customization can be useful for things like:

    • Adding custom Vite plugins.
    • Customising the dev server.
    • Customising the build process.

    Be careful with making changes to the Vite config, as it can break the Wasp's client build process. Check out the default Vite config here to see what you can change.

    Examples

    Below are some examples of how you can customize the Vite config.

    Changing the Dev Server Behaviour

    If you want to stop Vite from opening the browser automatically when you run wasp start, you can do that by customizing the open option.

    vite.config.js
    export default {
    server: {
    open: false,
    },
    }

    Custom Dev Server Port

    You have access to all of the Vite dev server options in your custom Vite config. You can change the dev server port by setting the port option.

    vite.config.js
    export default {
    server: {
    port: 4000,
    },
    }
    .env.server
    WASP_WEB_CLIENT_URL=http://localhost:4000
    Changing the dev server port

    ⚠️ Be careful when changing the dev server port, you'll need to update the WASP_WEB_CLIENT_URL env var in your .env.server file.

    Customising the Base Path

    If you, for example, want to serve the client from a different path than /, you can do that by customizing the base option.

    vite.config.js
    export default {
    base: '/my-app/',
    }
    - - + + \ No newline at end of file diff --git a/docs/project/customizing-app.html b/docs/project/customizing-app.html index 4e5b1cbeac..7327463d15 100644 --- a/docs/project/customizing-app.html +++ b/docs/project/customizing-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Customizing the App

    Each Wasp project can have only one app type declaration. It is used to configure your app and its components.

    app todoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ]
    }

    We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the API Reference.

    Changing the App Title

    You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the title field of your app declaration:

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "BookFace"
    }

    Adding Additional Lines to the Head

    If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the head field of your app declaration.

    An example of adding extra style sheets and scripts:

    app myApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "My App",
    head: [ // optional
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
    "<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
    ]
    }

    API Reference

    app todoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ],
    auth: {
    // ...
    },
    client: {
    // ...
    },
    server: {
    // ...
    },
    db: {
    // ...
    },
    emailSender: {
    // ...
    },
    webSocket: {
    // ...
    }
    }

    The app declaration has the following fields:

    • wasp: dict required Wasp compiler configuration. It is a dictionary with a single field:

      • version: string required

        The version specifies which versions of Wasp are compatible with the app. It should contain a valid SemVer range

        info

        For now, the version field only supports caret ranges (i.e., ^x.y.z). Support for the full specification will come in a future version of Wasp

    • title: string required

      Title of your app. It will appear in the browser tab, next to the favicon.

    • head: [string]

      List of additional lines (e.g. <link> or <script> tags) to be included in the <head> of your HTML document.

    The rest of the fields are covered in dedicated sections of the docs:

    - - + + \ No newline at end of file diff --git a/docs/project/dependencies.html b/docs/project/dependencies.html index 1f8f90df60..cb007983b2 100644 --- a/docs/project/dependencies.html +++ b/docs/project/dependencies.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Dependencies

    In a Wasp project, dependencies are defined in a standard way for JavaScript projects: using the package.json file, located at the root of your project. You can list your dependencies under the dependencies or devDependencies fields.

    Adding a New Dependency

    To add a new package, like date-fns (a great date handling library), you use npm:

    npm install date-fns

    This command will add the package in the dependencies section of your package.json file.

    You will notice that there are some other packages in the dependencies section, like react and wasp. These are the packages that Wasp uses internally, and you should not modify or remove them.

    Using Packages that are Already Used by Wasp Internally

    In the current version of Wasp, if Wasp is already internally using a certain dependency (e.g. React) with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version.

    If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp dictates exact versions of certain packages, so for example you can't choose the version of React you want to use.

    note

    We are currently working on a restructuring that will solve this and some other quirks: check issue #734 to follow our progress.

    - - + + \ No newline at end of file diff --git a/docs/project/env-vars.html b/docs/project/env-vars.html index b9b9ac7223..dc2c63c23f 100644 --- a/docs/project/env-vars.html +++ b/docs/project/env-vars.html @@ -19,8 +19,8 @@ - - + +
    @@ -28,7 +28,7 @@ By default, in the .gitignore file that comes with a new Wasp app, we ignore all dotenv files.

    Dotenv files

    dotenv files are a popular method for storing configuration: to learn more about them in general, check out the dotenv npm package.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, which will set them permanently, or you can set them temporarily by defining them at the start of a command (SOME_VAR_NAME=SOMEVALUE wasp start).

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects during development, you should use .env files instead. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env.client and .env.server files which made it easy to define and manage env vars. However, for production, .env.client and .env.server files will be ignored, and we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    When building for production .env.client will be ignored, since it is meant to be used only during development. Instead, you should provide the production client env vars directly to the build command that turns client code into static files:

    REACT_APP_SOME_VAR_NAME=somevalue REACT_APP_SOME_OTHER_VAR_NAME=someothervalue npm run build

    Check the deployment docs for more details.

    Also, notice that you can't and shouldn't provide env vars to the client code by setting them on the hosting provider where you deployed them (unlike server env vars, where this is how you should do it). Your client code will ignore those, as at that point client code is just static files.

    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME in your client code with the env var value you provided. This is done during the build process, so the value is embedded into the static files produced from the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    When building for production .env.server will be ignored, since it is meant to be used only during development.

    You can provide production env vars to your server code in production by defining them and making them available on the server where your server code is running.

    Setting this up will highly depend on where you are deploying your Wasp project, but in general it comes down to defining the env vars via mechanisms that your hosting provider provides.

    For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    - - + + \ No newline at end of file diff --git a/docs/project/server-config.html b/docs/project/server-config.html index 1b194d406e..2eebd2b44d 100644 --- a/docs/project/server-config.html +++ b/docs/project/server-config.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ExtImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server logic.

      src/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ExtImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    - - + + \ No newline at end of file diff --git a/docs/project/starter-templates.html b/docs/project/starter-templates.html index 5a030f090e..dc88e30321 100644 --- a/docs/project/starter-templates.html +++ b/docs/project/starter-templates.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    Simple starter template with a single page.
    [2] todo-ts
    Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
    [3] saas
    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.
    [4] embeddings
    Comes with code for generating vector embeddings and performing vector similarity search.
    [5] ai-generated
    🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)
    ▸ 1

    🐝 --- Creating your project from the "basic" template... -------------------------

    Created new Wasp app in ./MyFirstProject directory!

    To run your new app, do:
    cd MyFirstProject
    wasp db start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    OpenSaaS.sh template

    SaaS Template

    Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out https://opensaas.sh/ for more details.

    Features: Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Full-stack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    Todo App w/ Typescript

    A simple Todo App with Typescript and Full-stack Type Safety.

    Features: Auth (username/password), Full-stack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts

    AI Generated Starter 🤖

    Using the same tech as used on https://usemage.ai/, Wasp generates your custom starter template based on your project description. It will automatically generate your data model, auth, queries, actions and React pages.

    You will need to provide your own OpenAI API key to be able to use this template.

    Features: Generated using OpenAI's GPT models, Auth (username/password), Queries, Actions, Pages, Full-stack Type Safety

    - - + + \ No newline at end of file diff --git a/docs/project/static-assets.html b/docs/project/static-assets.html index abf274a3b2..541846d086 100644 --- a/docs/project/static-assets.html +++ b/docs/project/static-assets.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Static Asset Handling

    Importing an Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in the public directory at the root of your project:

    .
    └── public
    ├── favicon.ico
    └── robots.txt

    Assets in this directory will be served at root path / during development and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path
      • for example, public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    - - + + \ No newline at end of file diff --git a/docs/project/testing.html b/docs/project/testing.html index 590ad1bc25..a9886f21b8 100644 --- a/docs/project/testing.html +++ b/docs/project/testing.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a real-time UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "wasp/client/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "wasp/client/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import { getTasks } from "wasp/client/operations";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "wasp/client";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/Todo.jsx
    import { useQuery, getTasks } from "wasp/client/operations";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import { getTasks } from "wasp/client/operations";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/Todo.jsx
    import { api } from "wasp/client/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "wasp/client/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    - - + + \ No newline at end of file diff --git a/docs/quick-start.html b/docs/quick-start.html index 39ee1cde11..d425dd047f 100644 --- a/docs/quick-start.html +++ b/docs/quick-start.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Quick Start

    Installation

    Welcome, new Waspeteer 🐝!

    Let's create and run our first Wasp app in 3 short steps:

    1. To install Wasp on Linux / OSX / WSL (Windows), open your terminal and run:

      curl -sSL https://get.wasp-lang.dev/installer.sh | sh

      ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    2. Then, create a new app by running:

      wasp new
    3. Finally, run the app:

      cd <my-project-name>
      wasp start

    That's it 🎉 You have successfully created and served a new full-stack web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong with the installation, or if you have additional questions.

    Want an even faster start?

    Try out Wasp AI 🤖 to generate a new Wasp app in minutes just from a title and short description!

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser with GitHub Codespaces by following the intructions in our Tutorial App README

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. A version of Node.js must be >= 18.

    If you need it, we recommend using nvm for managing your Node.js installation version(s).

    A quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 20

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 20

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh
    Running Wasp on Mac with Mx chip (arm64)

    Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)? Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install Rosetta on your Mac if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal

    softwareupdate --install-rosetta

    Once Rosetta is installed, you should be able to run Wasp without any issues.

    - - + + \ No newline at end of file diff --git a/docs/telemetry.html b/docs/telemetry.html index c59f2823ef..95d7655e94 100644 --- a/docs/telemetry.html +++ b/docs/telemetry.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    Telemetry

    Overview

    The term telemetry refers to the collection of certain usage data to help improve the quality of a piece of software (in this case, Wasp).

    Our telemetry implementation is anonymized and very limited in its scope, focused on answering following questions:

    • How many people and how often: tried to install Wasp, use Wasp, have built a Wasp app, or have deployed one?
    • How many projects are created with Wasp?

    When and what is sent?

    • Information is sent via HTTPS request when wasp CLI command is invoked. Information is sent no more than twice in a period of 12 hours (sending is paused for 12 hours after last invocation, separately for wasp build command and for all other commands). Exact information as it is sent:
      {
      // Randomly generated, non-identifiable UUID representing a user.
      "distinct_id": "bf3fa7a8-1c11-4f82-9542-ec1a2d28786b",
      // Non-identifiable hash representing a project.
      "project_hash": "6d7e561d62b955d1",
      // True if command was `wasp build`, false otherwise.
      "is_build": true,
      // Captures `wasp deploy ...` args, but only those from the limited, pre-defined list of keywords.
      // Those are "fly", "setup", "create-db", "deploy" and "cmd". Everything else is ommited.
      "deploy_cmd_args": "fly;deploy",
      "wasp_version": "0.1.9.1",
      "os": "linux",
      // "CI" if running on CI, and whatever is the content of "WASP_TELEMETRY_CONTEXT" env var.
      // We use this to track when execution is happening in some special context, like on Gitpod, Replit or similar.
      "context": "CI"
      }
    • Information is also sent once via HTTPS request when wasp is installed via install.sh script. Exact information as it is sent:
      {
      // Randomly generated id.
      "distinct_id": "274701613078193779564259",
      "os": "linux"
      }

    Opting out

    You sharing the telemetry data with us means a lot to us, since it helps us understand how popular Wasp is, how it is being used, how the changes we are doing affect usage, how many new vs old users there are, and just in general how Wasp is doing. We look at these numbers every morning and they drive us to make Wasp better.

    However, if you wish to opt-out of telemetry, we understand! You can do so by setting the WASP_TELEMETRY_DISABLE environment variable to any value, e.g.:

    export WASP_TELEMETRY_DISABLE=1

    Future plans

    We don't have this implemented yet, but the next step will be to make telemetry go in two directions -> instead of just sending usage data to us, it will also at the same time check for any messages from our side (e.g. notification about new version of Wasp, or a security notice). Link to corresponding github issue.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/actions.html b/docs/tutorial/actions.html index d3d05c33c9..4419c23d0f 100644 --- a/docs/tutorial/actions.html +++ b/docs/tutorial/actions.html @@ -19,15 +19,15 @@ - - + +
    Version: 0.14.0

    6. Modifying Data

    In the previous section, you learned about using Queries to fetch data. Let's now learn about Actions so you can add and update tasks in the database.

    In this section, you will create:

    1. A Wasp Action that creates a new task.
    2. A React form that calls that Action when the user creates a task.

    Creating a New Action

    Creating an Action is very similar to creating a Query.

    Declaring an Action

    We must first declare the Action in main.wasp:

    main.wasp
    // ...

    action createTask {
    fn: import { createTask } from "@src/actions",
    entities: [Task]
    }

    Implementing an Action

    Let's now define a function for our createTask Action:

    src/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src directory.

    Invoking the Action on the Client

    Start by defining a form for creating new tasks.

    src/MainPage.jsx
    import { 
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    // ... MainPage, TaskView, TaskList ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike Queries, you can call Actions directly (without wrapping them in a hook) because they don't need reactivity. The rest is just regular React code.

    All that's left now is adding this form to the page component:

    src/MainPage.jsx
    import {
    createTask,
    getTasks,
    useQuery
    } from 'wasp/client/operations'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    // ... TaskView, TaskList, NewTaskForm ...

    Great work!

    You now have a form for creating new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser. You'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though you haven't written any code that does that! Wasp handles these automatic updates under the hood.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp keeps all your queries in sync with any changes made through Actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done.

    We'll create a new Action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an Action named updateTask that receives the task's id and its isDone status. You can see our implementation below.

    Solution

    Declaring the Action in main.wasp:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@src/actions",
    entities: [Task]
    }

    Implementing the Action on the server:

    src/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    You can now call updateTask from the React component:

    src/MainPage.jsx
    // ...
    import {
    updateTask,
    createTask,
    getTasks,
    useQuery,
    } from 'wasp/client/operations'

    // ... MainPage ...

    const TaskView = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error) {
    window.alert('Error while updating task: ' + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ... TaskList, NewTaskForm ...

    Awesome! You can now mark this task as done.

    It's time to make one final addition to your app: supporting multiple users.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/auth.html b/docs/tutorial/auth.html index 4f7cd380b2..4b493a71cc 100644 --- a/docs/tutorial/auth.html +++ b/docs/tutorial/auth.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    7. Adding Authentication

    Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support.

    To add users to your app, you must:

    • Create a User Entity.
    • Tell Wasp to use the Username and Password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify your Queries and Actions so users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it will create the auth related entities for you in the background. Nothing to do here!

    You must only add the User Entity to keep track of who owns which tasks:

    schema.prisma
    // ...

    model User {
    id Int @id @default(autoincrement())
    }

    Adding Auth to the Project

    Next, tell Wasp to use full-stack authentication:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "TodoApp",
    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used in a bit.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    Don't forget to update the database schema by running:

    wasp db migrate-dev

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@src/SignupPage"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@src/LoginPage"
    }

    Great, Wasp now knows these pages exist!

    Here's the React code for the pages you've just imported:

    src/LoginPage.jsx
    import { Link } from 'react-router-dom'
    import { LoginForm } from 'wasp/client/auth'

    export const LoginPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <LoginForm />
    <br />
    <span>
    I don't have an account yet (<Link to="/signup">go to signup</Link>).
    </span>
    </div>
    )
    }

    The signup page is very similar to the login page:

    src/SignupPage.jsx
    import { Link } from 'react-router-dom'
    import { SignupForm } from 'wasp/client/auth'

    export const SignupPage = () => {
    return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
    <SignupForm />
    <br />
    <span>
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </div>
    )
    }

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import { MainPage } from "@src/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/MainPage.jsx
    export const MainPage = ({ user }) => {
    // Do something with the user
    // ...
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    You'll notice that we now have a User entity in the database alongside the Task entity.

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because you haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    You might notice some extra Prisma models like Auth, AuthIdentity and Session that Wasp created for you. You don't need to care about these right now, but if you are curious, you can read more about them here.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    schema.prisma
    // ...

    model User {
    id Int @id @default(autoincrement())
    tasks Task[]
    }

    model Task {
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    user User? @relation(fields: [userId], references: [id])
    userId Int?
    }

    As always, you must migrate the database after changing the Entities:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database.

    This isn't recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional.

    Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/queries.js
    import { HttpError } from 'wasp/server'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
    })
    }
    src/actions.js
    import { HttpError } from 'wasp/server'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/MainPage.jsx
    // ...
    import { logout } from 'wasp/client/auth'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    - - + + \ No newline at end of file diff --git a/docs/tutorial/create.html b/docs/tutorial/create.html index 1cbe61295f..5224cc2c23 100644 --- a/docs/tutorial/create.html +++ b/docs/tutorial/create.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder page:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code of the app! Next, we'll take a closer look at how the project is structured.

    A note on supported languages

    Wasp supports both JavaScript and TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla JavaScript and TypeScript.

    Try it out:

    Welcome to JavaScript!

    You are now reading the JavaScript version of the docs. The site will remember your preference as you switch pages.

    You'll have a chance to change the language on every code snippet - both the snippets and the text will update accordingly.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/entities.html b/docs/tutorial/entities.html index d6ea2344aa..acc6820cb0 100644 --- a/docs/tutorial/entities.html +++ b/docs/tutorial/entities.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Wasp uses Prisma to talk to the database, and you define Entities by defining Prisma models in the schema.prisma file.

    Since our Todo app is all about tasks, we'll define a Task entity by adding a Task model in the schema.prisma file:

    schema.prisma
    // ...

    model Task {
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    }
    note

    Read more about how Wasp Entities work in the Entities section or how Wasp uses the schema.prisma file in the Prisma Schema File section.

    To update the database schema to include this entity, stop the wasp start process, if it's running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/pages.html b/docs/tutorial/pages.html index 6dbc0b80d1..904d635c9f 100644 --- a/docs/tutorial/pages.html +++ b/docs/tutorial/pages.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the named export from src/MainPage.tsx.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    export const MainPage = () => {
    // ...
    }

    This is a regular functional React component. It also uses the CSS file and a logo image that sit next to it in the src folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    Keep Wasp start running

    wasp start automatically picks up the changes you make, regenerates the code, and restarts the app. So keep it running in the background.

    It also improves your experience by tracking the working directory and ensuring the generated code/types are up to date with your changes.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import { HelloPage } from "@src/HelloPage"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/HelloPage.tsx and pass the URL parameter the same way as in React Router:

    src/HelloPage.jsx
    export const HelloPage = (props) =>  {
    return <div>Here's {props.match.params.name}!</div>
    }

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Now that you've seen how Wasp deals with Routes and Pages, it's finally time to build the Todo app.

    Start by cleaning up the starter project and removing unnecessary code and files.

    First, remove most of the code from the MainPage component:

    src/MainPage.jsx
    export const MainPage = () => {
    return <div>Hello world!</div>
    }

    At this point, the main page should look like this:

    Todo App - Hello World

    You can now delete redundant files: src/Main.css, src/waspLogo.png, and src/HelloPage.tsx (we won't need this page for the rest of the tutorial).

    Since src/HelloPage.tsx no longer exists, remove its route and page declarations from the main.wasp file.

    Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.13.0"
    },
    title: "TodoApp"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@src/MainPage"
    }

    Excellent work!

    You now have a basic understanding of Wasp and are ready to start building your TodoApp. We'll implement the app's core features in the following sections.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/project-structure.html b/docs/tutorial/project-structure.html index ee213dc6c0..c545ef0ffd 100644 --- a/docs/tutorial/project-structure.html +++ b/docs/tutorial/project-structure.html @@ -19,8 +19,8 @@ - - + +
    @@ -30,7 +30,7 @@ We'll keep it simple by placing everything in the root src directory.

    Many other files (e.g., tsconfig.json, vite-env.d.ts, .wasproot, etc.) help Wasp and the IDE improve your development experience with autocompletion, IntelliSense, and error reporting.

    The vite.config.ts file is used to configure Vite, Wasp's build tool of choice. We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in custom Vite config docs.

    The schema.prisma file is where you define your database schema using Prisma. We'll cover this a bit later in the tutorial.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's take a closer look at main.wasp

    main.wasp

    main.wasp is your app's definition file. It defines the app's central components and helps Wasp to do a lot of the legwork for you.

    The file is a list of declarations. Each declaration defines a part of your app.

    The default main.wasp file generated with wasp new on the previous page looks like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.14.0" // Pins the version of Wasp to use.
    },
    title: "TodoApp" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    // We specify that the React implementation of the page is exported from
    // `src/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@src` to reference files inside the `src` folder.
    component: import { MainPage } from "@src/MainPage"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that gets rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    - - + + \ No newline at end of file diff --git a/docs/tutorial/queries.html b/docs/tutorial/queries.html index fa4ffc9ddc..d9477fb397 100644 --- a/docs/tutorial/queries.html +++ b/docs/tutorial/queries.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them!

    The primary way of working with Entities in Wasp is with Queries and Actions, collectively known as Operations.

    Queries are used to read an entity, while Actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a Query.

    To list the tasks, you must:

    1. Create a Query that fetches the tasks from the database.
    2. Update the MainPage.tsx to use that Query and display the results.

    Defining the Query

    We'll create a new Query called getTasks. We'll need to declare the Query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // The path `@src/queries` resolves to `src/queries.js`.
    // No need to specify an extension.
    fn: import { getTasks } from "@src/queries",
    // Tell Wasp that this query reads from the `Task` entity. Wasp will
    // automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'asc' },
    })
    }

    Query function parameters:

    • args: object

      The arguments the caller passes to the Query.

    • context

      An object with extra information injected by Wasp. Its type depends on the Query declaration.

    Since the Query declaration in main.wasp says that the getTasks Query uses Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and Actions are NodeJS functions executed on the server.

    Invoking the Query On the Frontend

    While we implement Queries on the server, Wasp generates client-side functions that automatically take care of serialization, network calls, and cache invalidation, allowing you to call the server code like it's a regular function.

    This makes it easy for us to use the getTasks Query we just created in our React component:

    src/MainPage.jsx
    import { getTasks, useQuery } from 'wasp/client/operations'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const TaskView = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <TaskView task={task} key={idx} />
    ))}
    </div>
    )
    }

    Most of this code is regular React, the only exception being the special wasp imports:

    We could have called the Query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the Query changes. Remember that Wasp automatically refreshes Queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    - - + + \ No newline at end of file diff --git a/docs/vision.html b/docs/vision.html index 487bf6f2fd..db02b3a45b 100644 --- a/docs/vision.html +++ b/docs/vision.html @@ -19,8 +19,8 @@ - - + +
    @@ -33,7 +33,7 @@ They can be used inline (mixed with Wasp code) or provided via external files.
  • Has hatches (escape mechanisms) that allow you to customize your web app in all the right places, but remain hidden until you need them.
  • Entity (data model) is a first-class citizen - defined via custom Wasp syntax and it integrates very closely with the rest of the features, serving as one of the central concepts around which everything is built.
  • Out of the box support for CRUD UI based on the Entities, to get you quickly going, but also customizable to some level.
  • "Smart" operations (queries and actions) that in most cases automatically figure out when to update, and if not it is easy to define custom logic to compensate for that. User worries about client-server gap as little as possible.
  • Support, directly in Wasp, for declaratively defining simple components and operations.
  • Besides Wasp as a programming language, there will also be a visual builder that generates/edits Wasp code, allowing non-developers to participate in development. Since Wasp is declarative, we imagine such builder to naturally follow from Wasp language.
  • Server side rendering, caching, packaging, security, ... -> all those are taken care of by Wasp. You tell Wasp what you want, and Wasp figures out how to do it.
  • As simple deployment to production/staging as it gets.
  • While it comes with the official implementation(s), Wasp language will not be coupled with the single implementation. Others can provide implementations that compile to different web app stacks.
  • - - + + \ No newline at end of file diff --git a/docs/wasp-ai/creating-new-app.html b/docs/wasp-ai/creating-new-app.html index 2b2e59f3fd..99ca1b85e0 100644 --- a/docs/wasp-ai/creating-new-app.html +++ b/docs/wasp-ai/creating-new-app.html @@ -19,14 +19,14 @@ - - + +
    Version: 0.14.0

    Creating New App with AI

    Wasp comes with its own AI: Wasp AI, aka Mage (Magic web App GEnerator).

    Wasp AI allows you to create a new Wasp app from only a title and a short description (using GPT in the background)!

    There are two main ways to create a new Wasp app with Wasp AI:

    1. Free, open-source online app usemage.ai.
    2. Running wasp new on your machine and picking AI generation. For this you need to provide your own OpenAI API keys, but it allows for more flexibility (choosing GPT models).

    They both use the same logic in the background, so both approaches are equally "smart", the difference is just in the UI / settings.

    info

    Wasp AI is an experimental feature. Apps that Wasp AI generates can have mistakes (proportional to their complexity), but even then they can often serve as a great starting point (once you fix the mistakes) or an interesting way to explore how to implement stuff in Wasp.

    usemage.ai

    1. Describe your app 2. Pick the color 3. Generate your app 🚀

    Mage is an open-source app with which you can create new Wasp apps from just a short title and description.

    It is completely free for you - it uses our OpenAI API keys and we take on the costs.

    Once you provide an app title, app description, and choose some basic settings, your new Wasp app will be created for you in a matter of minutes and you will be able to download it to your machine and keep working on it!

    If you want to know more, check this blog post for more details on how Mage works, or this blog post for a high-level overview of how we implemented it.

    Wasp CLI

    You can create a new Wasp app using Wasp AI by running wasp new in your terminal and picking AI generation.

    If you don't have them set yet, wasp will ask you to provide (via ENV vars) your OpenAI API keys (which it will use to query GPT).

    Then, after providing a title and description for your Wasp app, the new app will be generated on your disk!

    wasp-cli-ai-input wasp-cli-ai-generation

    - - + + \ No newline at end of file diff --git a/docs/wasp-ai/developing-existing-app.html b/docs/wasp-ai/developing-existing-app.html index d28768f4ea..96aeb1d217 100644 --- a/docs/wasp-ai/developing-existing-app.html +++ b/docs/wasp-ai/developing-existing-app.html @@ -19,13 +19,13 @@ - - + +
    Version: 0.14.0

    Developing Existing App with AI

    While Wasp AI doesn't at the moment offer any additional help for developing your Wasp app with AI beyond initial generation, this is something we are exploring actively.

    In the meantime, while waiting for Wasp AI to add support for this, we suggest checking out aider, which is an AI pair programming tool in your terminal. This is a third-party tool, not affiliated with Wasp in any way, but we and some of Wasp users have found that it can be helpful when working on Wasp apps.

    - - + + \ No newline at end of file diff --git a/docs/writingguide.html b/docs/writingguide.html index 8fc9e84a51..fb65850d49 100644 --- a/docs/writingguide.html +++ b/docs/writingguide.html @@ -19,8 +19,8 @@ - - + +
    @@ -53,7 +53,7 @@ Many of our titles are currently in title-case, we should start phasing those out.
  • Use the Oxford comma (e.g., "a, b, and c" instead of "a, b and c"). Why the Oxford comma is important
  • Content and Communication

    • Excellence comes from iteration. First drafts are always bad, but writing them is a vital part of the process. It's extremely difficult to avoid the slow progression of Bad -> OK -> Good -> Great -> Inspiring -> Transcendent.
    • Only wait until something is "Good" before publishing. Vue's guide originally says: "The community will help you push it further down the chain." We don't yet have that luxury, as our community isn't large enough. Still, we can't afford to invest too much time into the docs, so "Good" will have to do for now.

    Processes

    • Ideally, you should write the docs before you implement the feature. This will help you see the feature from the user's perspective and better spot the API's deficiencies and improvement potential. If something is difficult to explain, it's most likely difficult to understand. If it is difficult to understand, there might be a better way of designing it.
    • Try not to get defensive when receiving feedback. Our writing can be very personal to us, but if we get upset with the people who help us improve it, they will either stop giving feedback or start limiting the kind of feedback they give.
    • Proofread your work before showing it to others (and use Grammarly). If you show someone work with many spelling/grammar mistakes, you'll get feedback about spelling grammar/mistakes instead of more valuable notes about whether the writing is achieving your goals.
    • When you ask people for feedback, tell reviewers:
      • What you're trying to do.
      • What your fears are.
      • Which balances you're trying to strike.
    • Do your best to come up with a good and straightforward way to say something. Again, this will help the reviewer focus on high-level issues instead of rephrasing your sentences.
    • Read and correct your text several times before submitting it (preferably with some time between the readings). This is similar to proofreading but has more to do with content and communication style than grammar. A time offset is beneficial because it removes the text from your short-term memory, helping you view it more objectively.
    • It's OK to ask AI to improve your text. Just make sure to check it and correct it. You should always sign off on the last version.
    • When someone reports a problem, there is almost always a problem, even if the solution they proposed isn't quite right. Keep asking follow-up questions to learn more.
    • People need to feel safe asking questions when contributing/reviewing content. Here's how you can do that:
      • Thank people for their contributions/reviews, even if you're feeling grumpy. For example:
        • "Great question!"
        • "Thanks for taking the time to explain. 🙂"
        • "This is actually intentional, but thanks for taking the time to contribute. 😊"
      • Listen to what people are saying and mirror if you're not sure you're understanding correctly. This can help validate people's feelings and experiences while also understanding if you're understanding them correctly.
      • Use a lot of positive and empathetic emojis. It's always better to seem a little strange than mean or impatient. This primarily applies to Wasp team members speaking to outside contributors. Since most of the core team knows each other pretty well, there's no need to go overboard with the emojis and pleasantries.
      • Kindly communicate rules/boundaries. If someone behaves in a way that's abusive/inappropriate, respond only with kindness and maturity, but also make it clear that this behavior is not acceptable and what will happen (according to the code of conduct) if they continue behaving poorly.
    • All docs must go through the review cycle, preferably with more than a single reviewer. Different people focus on different things. Some of us are great at coming up with examples, others easily come up with analogies and explain complex topics, some have a clear and concise writing style, etc. Therefore, try to get at least two or three people to review your document.

    Linking to pages in the docs

    Always use relative links (e.g. ../../overview.md) to link to other pages, unless you are writing a reusable snippet.

    Never use absolute links starting with /docs because they break our versioned docs, instead use links "absolute to the file root".

    Writing a link "absolute to the file root":

    1. Write an absolute link, start from the file root (e.g. / represents the docs folder)
    2. Include the extension (e.g. .md)

    For example, /docs/introduction should be written as /introduction/introduction.md because this file is located at ./docs/introduction/introduction.md.

    Or another example /docs/auth/entities#accessing-the-auth-fields becomes /auth/entities/entities.md#accessing-the-auth-fields. This file is located at ./docs/auth/entities/entities.md.

    Possible improvements

    • Some parts of our docs don't follow all the guidelines outlined in this document. There's no need to start fixing all the issues right away. We can slowly improve the docs as we edit them.
    • We've discussed having a git repo with all the example code in the docs. This should make copying, pasting, testing, and maintaining code snippets easier.
    - - + + \ No newline at end of file diff --git a/img/.DS_Store b/img/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..53fa41e3a06f6452b65e592831ff269a13321683 GIT binary patch literal 10244 zcmeHMTWl0n7(V~Bz|2zUbfC~yFe6P`q+8r=yS)HHuUt}0f$g13-Q5}5f!UdLXLbuV z#H#TH(5NpyDBfO;MjteSk@%pI#KZ?^R5bcx^hFcVH;sw@=bYJ6wxvm_6&_}iIsdu* z|Jga;`TleMbCwVS?HMghh)M_%@ugBO!eowzw$pdkBX~9Up#-!iBtslh%Tj%owqM~R z;3ME8;3ME8;3IG^Ab@)|eZ-Yq`l*kAkARQBGy-&eQ1PX*)svH4vZVtPb_76Kh}G=D zJe~uXjq1r(Pfl{lOtDXXdH~N9ydws1b8J_sI?7g0PIAf332<`)-Y|Fc$mJ6g%}q@~H5_U<6I3U{q58U@8fj=cb4C(ND<9f^Xyl}swCr;n9PkzxvWl0d z)AN|SV*pGi+ra@SD9HivO%4Dluu|#k9|(-dN)|`ilX4B)a{BeOgK|VJb`Xty3EMi5 z)@l1d!ZtHW%NdbN5-~H8F=MXYW|~psaorho^;0g*4Lf$)?O=+72HMiP5n0YS`XL<+ zjI7>?X3pB_;qeSe?-Z48+*DN)ifr52v8TIdYSrpuIiRdrJCHRTBW~(LF(;v0nqiIh zjT@RfHjGePu5sMZ(~?*M!Cott)TiWi>&wb36ou>Dw8`}{n28&YWeoSUTq%lE^6KJM z_bY1Ph=$+vh^^7yG*NzEVl9=5svM>*9cdj?%`>Q09t`w4I4n_)am8R@YoPZeazTPMBDQ(Iz`m_6Ej@y&c zE&54Ucv~m74Lf78wB9q0T=c$ZFV?c@!g^}dr5Ucx8WD~Cs-g_h?%FhsjiGGadRaNB zL`C_kh%;kYSp)fxGtS*WtZd!F=Y%0z*&5;{2{+1evK6<*z2pdbR|@^`Jh?z#C2x}t z$YpYcd`Yg8Z^?J$2l6BNmHa{ehC)~c#UMioR6sRshAmJ74bTd0&<@>j01iSFdf_O< zAptrZ2OBbwg;Q`Eo`SP*4xWYQ;3aq+-hemZEw}_9!N+hJK8LGt4W{6G_ziBr@9>vU zC@d3}3&nygD8d?{N~jh>LPXffKU~N;)(|h}QehY77UNjn$|9I^IaWp2?mhR0V_h$z zu2h1qRC<5q=GwZ(?YpKa$2pp#(8na*2F`oSs5X|4e=%H^@IAB9kjXg(}A7 z8mNUvXo6Q574i@&B_}Y_urRi;SzJMb6$cTcNF~@B=(0A8 zICmSja7#T#8j*U2TeulPj5N30L8P&OA)BM&DDGV;PED3Bz&dGnc6yrMX6Zk>Z$o?RZM3N9&0TvALt$i#3tsU=ygsYNc)b5M%c9>=9CcN!|f z)jESOU6Tw{qJ}3>CTA+GMtdEPO%AHjC0<7Z+G`AZOx^AIYp82Rjz&U@oWFU;m|N>9 z*XLEtn+~e)KTC>00qP>RVOfaD;w4PnZV}V}BZ}#fEn+w+7>eWqh$;{D?nWN;l`Ahz1z}>i5^UweO a`5(hSybaHbbmlw(&j<}?-Sp9rh`+45j-56M!Yu{P#1HNqmU$=gq*S??D-|v=zKl_eeHq7EYJaWQZvO>J_ zA`Bv2rVhrhH%m9C)5p8RCwrq$R}0^d>la58&c1eRl6-s$qWp?tOkzAA_bb4U?YyD{ z4Kq!EvuSTnIq+@!Y=0~u!b9Ii|KVZ|__Z4p=}F4P$R#7lD#6Px#a~;S4Se4@*dEdX zXmdylaLNdNJ*)zsH-KOJVR63n!koe?5&}wM4UM@*c7}q=;&?g5UTLC({Y&9s8- zf=c4G^*P0*$<+L8|5^%wAG`btq9UqN?d`>@i=EuE!Wn;}IHUxCpZi^1r9j}p@L)}4 zb!KjUTxxd2`N8ml+5sw^3=ri(D>BfogF1&>e4Dk zYSP;Ba=MCYCK{0m{*s!q3i`@QhALaDy^NyV-XX5mZWex_?sgtlpZBY}mb$UYK|tUU zpS;NOLPu0$;P^-#5O}_^+->Os2#N6l0&iZf7lFXj{=Ul3`<1WPt*hgyvdZ-N>6VYX z72wl4B_GS@)7tyP&im~$9t#aR9r-|K>1cmdZf3ZRg&rIn?0-wFC?|@Fij0qkO;1ND zCc>kkA|oj-P+OkP&PuPZqokuL@BPaFnpG1-Pa7a~#5CTulxFQgtpXA*$g8dNtkxIw%6t*H^}J)$3qZ zIaRV>n>rx$pF#h(!rJm6p%dYHZeV4^Q?~g%?~`t-lkn!#CKU2KeXGP%BJAt*H8!&F z6X?{4uBzJ!XbM(apN3L98!w#d zm#rl5+2}Mb^9ByJ@Uu7ZGp`!>POn)+N7qzOTOM9fmO!-Iw->q=O-|gJYtBiu(q%zc znD0@CRZsN05m586s%IebZc7wZb(FS3RLtZG(r1@PJR`p8I26D43dOKzOWYv`F;JJD z&odK6P>p};{2;J-y2TiZj`~>u9me!FqdDH=fG}gOi7n845Q<$J-nflS#wdiBXFts1 zP)A<}d3lvPLTzu#K@8BkfsuZvF39Zz( z0Reu2s$4O!9sr7>?-jwBfyq26eA+lXwIFmdjTgKAp0XPLO+B0*cFU*@?&Vd+2&vn(bJ zVM!vfXlqccieF+HM^FHIilm3c+SrjzJea4re7s#JnM0Y{{Ij z7@@ZY8{2*iVJ@>I`ddX_G;;IsuB5X)@OBomK-vcLpK@Mf3TEo&SDG;gsVaFZ70nDI z9Xa*Vtdhpe%M8rStJPCkbg&F}U0%N&aPcxuzNUunYMuJfSGgW5W(AbD(=D}hkzmse z^!>RCGt3#%ug5D(ARIR?w$8i_Od42*(%2+Doe%X>e$;qZ_Le4ePoveA|D9nsb^cbb zQ_}NiS0{2X58wdsjs<1m)u~-^8Nobe4}du}@xMsPum_#JwRkI`Q(t?e_%M#lV@8EZ z?ewfV{9tOE+3d856?@hsO~ogc<2q589lAHP%7p$RqBzW)Dm|%K4$44>*lOx(gAU1yXxTxjvw17T--G%}0UH&EO`675Rpll^egvD1Ah;y^<^`bU^_CN+ zxiOfH4Owpokf5P0I6R8YrLQIYs|~gvjku_91b-xytg7X6_+u#iF*hFyhK2k}lLuSjB zRBEdH9TP*Yd__;NL`x+u&;Y&J!dJYI0(jBl#iR!+5Le)6n#6FRsLD!mbA9|!h#yya zS_h$6p-W_nCiBq2C;&=#G){jg-7>>QJ|07(Dhl3@-5;z7Hw&w^QnMk7utVPALbpQq zKOH~*kbv4>2)cp(kCkXx8lo!R5Lx373I`AxLS)`NdCz=a2D#?PLx?@d|E)S-vAcfh zS>eMhDY|g2xtZ5agX))ifm^NDVDKq^|NBL;Ok{13B61z!>rL~5R}Y_2gAG*rveNyE z5DS1p}=Ejg0n?@`A zDEMEp8@blTw5qz;Mw_${fy(o|1fQlAEU9g`W{Xm2_Vq+SRAf;x{va4ixkU#%3UT5B z;h1{Xdnys~{%3{V{O&3RHP$^0Mi;#$<(!yKOEMxnqLvuv7aLOqGubZvr`^W!oCCJG zR1rayqmPWiYjc5he+f-rH}Y&l6Q*{(1q5ts`eH&y8H9IQ&Lp}KQ3C+_@1V{7)60C|Nh#UR3e zF|%4~U<9+oXqyPT;CrAv&G<@@Yy1 z@a9;|gE=U-AU!NhG;)XYdb64iwHDBo%$sgHW9wJt)q-HM-^3w|tS4~NzvL1#lk@Gu zW3?@+EJE2rV7tguPI$mKF^GVidIyWC-U+vi@nQs!es%<-JX6YvOw+7opV5}6FGtn7 z2>q^HnJY@y7x5wZr0_pk|D1!8s1y}T3Uo+;nVhn1Dj`X8 zpJn#;iue4}2hrPL52=L#*Yz9eP%1IW?c?v3a76Y<k~+)>NCBux3LM;r`M;E zO(J5psu|EfS;J;rNxWCh`F23rwx@OtubDN?UVX*3L|dvR#IY5ZJ#_a z>z3q-AmZi;6wbDLip}MbFEIKI`; z>Nj!5rWUnZds-q`Kz~X7W)a4D>0^t44}cnD?F0#q86PRDXWN{Bjtk>DVhJ6}n?m9K zjezsOPsWG2NFf8x{`I312 z02pS*ZhF2DoGLCQWfxpHEv5eT6I%$nwD?1UCwmJ5cPWn|HuC}aLuJG_V4$<%M1k-~ zP_>MaT?TRwtbRvHVWvLd4voElH=}cw$qTPGm`$Z^v0D0-_^x$Yd>{!fo&bqeCn_)z z4OijKoRCw&%ozd{0{pX}%szj<09DaSb-=4!Ass};es_(HI8>nrj3l{Mlt=1z1ejFp;BL1$#^l`^i1p49NVScz~= zo-Y#u#65nKqE`#Xg%gA5I$}$0qzFb?h*DYkvR&9`N=_=&<(w(;4E2cR z6M|Y%HktV1o|Qp#Cg-wUYc^(vp=3mfkhn`Jzp)sp|n>wK^6a}o&# z*AqbD2!lJnA`okuWLqZ7I+&9CH=k+q!4WP?*0-biJboU5&4e}H>)E1GJ4_g$OAC$5 zn<&y-x$ey-uG>IiG6qM(fjiBw$#X-A{D6wrgKAxRcw0j^rH_z1a3pNC8NJ9I9&5to zSMhAGJ5_M-i%z*U615W*(C3^)T_tEYIh8V9@&JIB6eh#X2|;L|LARwlbQ;TS%+63P z4N0(1$Nd#TUH#qR!Z9=09LAz3)>MUF%ff3r0X3>3qouqH#x}?=xf}1B{SOHca4}bb za~JbpK)~D6@ucd{%)GsxUBcyc5QG1Rh~9s&Vyq8Bjxl6r1zG)9V?C+kJs{?ZcX%{; zszy#IGK(rDEgR0m;4;GaW;Q<|uk9}pWVZ;|9x86kKk~rHGs*gdIT4hLx0! zS!9);>qR)z@SOU9fo*|nk%aXAX5c}`RG;<7X}r@}#m%~)CYja6^0da*cY@2JY=Ix_ zK?kyHCPoRZ5c7dM<$>RklS~c+ihwPDT~%6-Z^RN^;Z6zAW@}Fl@iL1tLD((gJ#*j? zY77D|v*hMn^n!)otu>;-=`nL5#a-HVCz ziO%FG%r*ww#uvsd4J`i)amP~`=%DYmzMdU@z#I{f0040qIDN@K(UmYmlUy|AY8;YB z;kjfY9d2DcnkLq{v}p~+!~d)29RBh|1?ZLHg$Y=irweCbGdL}a`-=Ia$^i zyrAb~aOCX7bIUF)z6Hf5lEz&2^2WUtvW$ClPDl)0#UBept~iDKn*kb^bmICA6#WL9 zA16!ryBS-9#hifOjdkw+hyiY~J_O$#T%g|l&{2&0@oL2SiEb?Dy}vPJv&+K$q{;dDaODm$btufiV0&4dK;;Xw;IZhK3=b!IvT@ zAoIVx4>k1`7+xR{t(}54|N1Jg%>L6Y?9+9y|4aFVpUKSK%~i1ALd(u2Ykxu&?YqUO zo2SalKe)D0OeAt16rbMY$$63J28CDBN}dq>S_owflzA04z&VR5J zMnv%z65=Y}i>9ka_1UWo;k*;+?1sq>XOSa)$A9+C>bJdZ>mNL}V?)@i%M@ho*hX|U z*wENF&7UDUQUx&Nu!M-K&rPfYtvrNR)jy4m7pKdxVrI9o6ag6jNoTRJxQ^D|`-cz~ z);D3^m@GptG=GV~^G$sGW3Q-by=C-rKuc$jFUvNRR)mjF&@P}fNj?Tc(rbithht>e zRiNWL$+(xEXgYB=PDwp$rG2Wh(kXivNBSRH{gLG<8XpXRWs3?K^%Dq3kmHy;q+HMm zUG_(t+@QGmPNKieWB(Cd!3sXO4zbCW_{Y^RMt|DJ?^KKNtJ!{8-^82$57T|I_)oA& zOG!>V&B`}!I9ik)3BEX@Tn5P^-xZ(Fp$ot$YPG{x4u7b6cBgN_V`|AlT!}lxUXYbw zfdZoUi72oYoDhE`ku<7#%j?53#N0t>_%zCSKfSD&N*6*`GVWFDUx5}Ij*(!zCBg<- zV|aK8k^1GYkf*B7_j#649|6>KbgommC;`ELs{g+2jrYii@@K8g|CQ|AJI3fz)CON2 zliJsZY%{l~9)VD0{cCF<8NAp_bFNB=$2V-wd=FsLM@fI_8B{Q2+fv|-a)FwBf`m{X zWs3vBq3t;E+dJQUy{@w~=8m!CW;D{|m@)XtMC6F!aH#=V=lbsWIG(ghDLXEmKp5C| zDDd<1Xz2dFUJQPe$}R$F|9NuG1cXWv#BY|$bg7Kf$N&2W;7|Hts0yNwaiMi|KT_f66~E(XuHC%mh_M8#s~_6G>&jeuL4hE9U$Z#kiQXwp+lzTX}Hz zV|OsI(OM)Y&Hv5((5(~Wu;#>9xW|wGd$#ddlo80BP@qf_eszi9zwAHu`QYEjU8rHy z>;l8K3hnnmIX1*I=h&Jw5_VHN25YF8zfI?RW!9=HqLoepO?+*vxZ| z#_alsInl+odsb#h`bq3mk26j2UD@+BgM0PG6FcE(03QJ$z3nUZmM%7rnf=OT2slUbHM=;t3k*CbfDf^dU0mvS>+j^1;Z z+hHBPtf%T4v~#m^{F7Z&s<|Y78X!b@un0Q;87jF6wZe5kBcTe$`)r-?y{(AeS_b&oUi7#v<=;W^aNcx`NIK5_XS^cVLApN-^4NG)K zl1=5Mvlqu@rj-;I>WRM>T*UA|N^TDI2>h6al|L=%=_>+1+QzhpPR*AdwW3v{(Mp^K z7WK{Va^CNd+2GLts>jJ4d4lFDH5YQ&x$BaPmF6558W_{O)4|^)^&5njHXb4HV??_1 z`Gso9khnin0-b-wPnfjsI>XD7F^9F3s^ZXvF#U1aQUtzb4%0E2@QxiO)=m)O-fZX@ ziQw%t-3Uj0t{6MuU*>6Cx*X1zD2+lFqb)J~I3d|UoUX@!JjT&WgMw9Yix)@UqI;Oq zvb?_tEi%*A)IuZTt3^3dyWNUfk#ffE$B^#0(Fr+q6Ze1H{x#DdH|=v}CAB5qn4>@kU?GnVr-u02KJL9% zJUUP9i(Yfvxb2YbAT=66ihsWt&-(c=?ZJ)Vyi3Ln9rOojJUUCfi)-n{E|w4LLMYS& z8;tU3D<5IHglw5p?w8v9`FV(p=oF@Efj$2ouklfmCYO!JC?I{jRo#DU;MC6u}Mt_m;whO zAN^`e@%>uhNxw_E1J(EnC-$5$oW%N*c)H)uEJoGwkU@x(RZmfKt(ArV{X{F3nD-ti zo$nY2gOb_JP99KxR+|h#DB&c3VX^A4d`25jSo_FM^^W_S;p$9K5P_I3>gOjk;q*yw z6^`a!Px_=T<&N@(E13#qba(L1{f~O7zF6zCG!R|#^<(ap_Z1=?qy$k z!LPC<1V^MS3&K%A@D4ZV^5L($>LbH9FhJi|;c67FdAH!>w3Wr;W(fXHeGh<#8HhB6 z$*=TBlE%4Tl?xHGU@Hv;z*+o(wLGeRg-+|t)Uw#UR@4q^b!hxgr_=@ z1E%gp4ph%zmHJFMUF)sGYnOB*x*=0ITy=2@OTS?$nuC$`}$+EfIDmmoY!$ zHj5yickS+ifi9;QYjHa?7RADAvoO^4!fbMh965eQo~X|+E^2L%J!j8>$7K>jc0$!f zW&eIo$NXkfd}N(V%f6>Y`Q6G2@>j8OWP7jx66|jaLHN>gh2+#LB8UD|*crMe*vKDI z4*qbkQAH}9AbXuG#frUlS0rc!GulX=>S3u_IGV5*e< z>e22lPvyT(pzHcDxE|Y zj>ldwqhJ@j4!;f2J}&uzFs}cRgQylgW8_4lM-?kyig;}4$I#JqrYnP21EyqO=(g#K zt0GcyYc2*xVeG*pP;2;ki1Ba0-c3;`g_0j5!p87Y9=~UXVFs*^5iskj&mWIZQsZC(T=TDkxT)=$sXag#%gInL6UL{(6!Q;*7l?<$}zQakhT2 zO&IUvTPx8f8W>L6+2s+1fxCKdN>!<=1>7=&jB`lO0z%|jHv(?~7$}~i_fbk;5Qpd1 zT;}o*eKG!?AC&=-0|67^huyu4(+_Z7}G2f zkeO*%b&lw8vo*1%^{Ye8xbj;FA_H+;9w&t2u7GIwG>qB zZo5?GAu}w)A#GRwKzQ>2)nk0fb)cR9IYDOG5|b`YbJ5@& zhg-@iFv4JyDyq~BZKJT?%k{_GwaHF>kyKipgT@_f^ZJ`~g`7-LhtG2|=UH+0l2O6G z2-|&pUx0F^dakj=w7jyf*3$3#cpLcrmBA=(z~rdoH^L_c@J|%3j5C?5c5-#Yn|E4Z zfSi0`di`SHrp;j^eLR4{7+13z4t^r8s|&7faUlK}65rv@S(;_@b3N!mI4Vo6mn6nlgMkqNa@{t!f2Hd9M~lGnq9`f)Hh|5?advFAhX& z?2CXm5Q+UEZbovrq#+Td0>rz~bw_&KERlAWDhFi~x9-oH5eqU~6dVvM>kwHr-cQ_e z|5i}xAW}JRd6v}3XKDSbfRu#lZiz0k&FUN=#ws&~$2@7TCm?Y$>!rQeW$F`3$fVqRwdZv&><4`LThC zjWbOb;qYLKhEcsQMy0j{-^oW3RQh&xnaqrzHV{N*ZMJX}+! z0pS+pc2j^DEvqP41V&L~nWJay6A_0kUa&Z_IkP-(Jrd#)aL<*vPu=J+$(d*~v{TWv5$OrA`uB?pmL=6-5&3YThThujOv3E9s;lcf1>M- zM-qo)EX692zv5}lb)t)MypG2Zdk6tuiwDB+Y()-8-$`&$>rp(6t(ze%)(1uiS#y#? zOQeSm$PC_DF){pn0K&Ei1YvqZXk4Nm6)u2x#hq+R4=v{@s(0XuHG+*^B7Ip#3-O>@ zi|*w*f*e;pWOGmSbQrPPdLnFF2tuT*;LB#cy!s>f>D!O1P?X z-XuuoBzM6&w=6X@C8*NOE#1Zs7NDrh@DQ;qQ7&fDVlx`s2s#V+d^dYDK2^f6#eKY%0CYGB_E?63{=(H+r1+JZxr2K6|qc)_7c2va+-kqGQJG% z{l*j>{NUfW>1tM{K&CLVvw94Mv|e-Xw+~scOWl2qD2U1XO!?7IWR*;L`$Ip}ScWkc zKGoS;V_ew7_L?G2C3SD>Uf$bO(YR47rm@m$h{F~~B#3)iGpS&TGug@FH!*^n8_sZL z=WQ)3-ukAQ2rADIiLH(%s9Sfzrp^C^G4gvdwDArsrj^Ei(pRq+@_af%Oy?^Nn({6$ zzq_dtWn5?J$&&=!UDAmxl={x3^@&$wy`Rx!>`rKi{1p-~E*jzdmLh%<5UB;aJVf5$ zj{NiARab<(oQg^LM^s+lA5OrG^=S^#A7wjI)e)fJ?^rAD?8PjJRsQ2UI}&zb1xj=I zi8z}vx+rNQe`+T}Jy1FdJpt!_0iK@%$wB5N;96XIws}3~taJZarF75_qw#;Xh*EqF ze`f3mrvA?INuFzKb&y6MBl;0Dh$_6xvBKb+yxO4(Rh zCT!C@Gr?b#@ZVu$w&~P5nLCxS!5PVyFhohG<%KJ|e>b>#nFj~vAb^-*%F=dU6$M&< zfa0bM$3NQ#jxaOwvTc>ekiJ4#5HR0^nF9jKn;%f6r-*H2z;Ei4 zaLZp7t1>pJupb86|303W0Vipx`*KM_jy?MHT)a-on)~Z^@Ogim>-?&EZgG4Ab*|aDkiW zD3kSJhZ4HiwU5?ecn02%oz_nx|FBgcC*TGEgL4$i)7U$gGcIHdME3 zt=;%WYMFJU(NMM=`kvB+|W^OR#?6%k_@tz}+?mK3ASPdO}L3;41GQYVtvPE~3`71m5q9^98@XJyxEw zdeL=U@0`Y=9Ub&_1iB@+`W(idvmsTef9UEkV}e;_YSwv~u`VV)RaW(z(jt!rGn^vNN(W)z2 zOg{#dL`KODE-M1M?mX5je2n23cHDjR$E-_thz=4{r5hsd7d{)>L?!!TIo*L-(Kz2_ zFl+=B*B*;xOy8h!q!`RPRSkikwqxZk{@Qu480 z(Md0Jn7V8dltsB3%J*c%)^WD-W`uWIBY|FH4^z~Gd4m`Ld8?DD5Fk;u`yTl~X0t)3 zw*Fz7hQER5!E7+mwg(@CzXt*}SiD3pGtY`jt@w(I5C0fUQb%u{+W>WXhlgip`W3;0 zc|=c1PfU(ow4;+01Yu)ehp?o&`$NT}2>cyE_;!^LYY|+fA21 zen;Y@&2MDx}wQ!wdes$%=hC>WKtN@`2(cS(QLn=la@n2PUzq>S(7blN&J zqT5Fx2;4v>r|EOJi^hy@Oy{(jZOnDVLFz59hWKwhxceS+)<}}WrH-OaQK`tAc?-Kt z>6V7%McM|s1ka+d3xnQLcH3=1rR`u>F)LL#Hegp@nV-WTF5(XqpwFPEa*uj!V??;0 za;(G7USJ3EWiIYS9^}>1#O75LCY)!MVm-{B;ZwRF{Y|@3%rd zk$$AT=0a0fhPwB69O%9{4TB?{pS#0TTMdmJ<2KFO#tue9WUA zep*~O3owU+zYCzetIWNFIYOrF_3bBTpjz0Q(VGv&IJdPLnB5^l2ni#+zJiYTSjug9 z7Je}TbTm9a)yxzSD?ehSe^;6zzZ`Wc=)%NC@bRhSPVl#7M$~`^IC(NbHeh{CK?0gO ze(}JwK>}lR_hL5dB>zpoCwSh``#avl`wD$J-mEId~o&^kq`_w$^I!kT9HdIUm zE5RB5{hnI<^bn#EsVFQ^LnPfa-%rhgd!i1gN-0678PCuc7HaS(Q~5`IQ@y>3-)Gh#p4%gplql`9ZS%$GJ&2jgMJ zc_NP~#(vUL_XlL9Kiokx*MW)Q_ST=m-*Z@RyE8`?W0|Gu$JhqylhXGOaiAkjP#Qnu z#A@s%pHC;R_}@x3t3uL!^PDDy@`0P*v*=r+r5aY{zv+~F%@tG$Rzw1w`(_g@DsnUp zl0et#ujBhyGU5&R0PO9(-*3hTH>n5-_d5Yb{=rTXtrBGGI63|hyLfHO4>!|I_t26Q zZs!R-EL_^lEv@T~o-7O3RxleH-0cPD>d!PDU?=8Jp96aeZM?ToPWW*X)VVSBBhxLl zl_8_Au>q)lTq*2zR>FmR!nrt1@xckxDe&zjpj3*C??oTzT-uq7s+#KzOvZ+LP{wPh zY<%Ub7;ZU{%r-`K?jCw|8h;J^m%H`q>~>l_h3;bR0(Gri2p@?TJE4W9s&BcLu2!{b zkVj^wg5rBexb3*o@8eY-{{@Wl%|1=CpUwshd~Z`lby9wKe%)FYN+26F{$oHX`$j*| zeS8`BcyzA_9OyHUQpJeL?wEvceViSG#Y1$QchX_nhUPf$rJf#A9Ex;ZC|dS%Ra4n< zQ0N$%tSv?31f{fk%hICDvW*(6M=F^qVUKm7E#@~A9Wm9us0YIJqAKQ^DC|a11s1Pd zenCG$jQ7cXeb6eNQw>d!J%*foot!)R4D3Qhn2;@_e1~nM04mG*AAjGbmYe^fQ7;*l zoJy->EtPGXJWi(711M;#nAVRmWkDeH^^P_$SyuC4rATinkb!SB;~6#%gW%k|(r!su z;_HxDSTx$o>-KtGWRSA?H~cVb!m-&G;qE|3&)ufHh4F!i<7_wP*plNw`JG!@?6Nvn z^imx-$_~*YJbmCFa^^UL_)znL47HSH_Lx=&*$p}DQ0{*wdipQH?)kFZ$pQhO%}h8> z7e{1X(x<@L8Nq6OG;vUjoo@@`O^-^4%~pra?3Z$fF2_r}VqdkUXKhOFpv+MvLyU|d z6&Z?ue%t5LRnRv{vzcz9!;}TO^u}Lz=O4wx;B9c&`Gy&2eWaY)Hx5~C4*|Z1m#8Op*5JjbhDe#3>Z7p(u zC#x*q;93jHCXq?l3Fra&mkdb)A|uoWx%c_yt;`jSw^8}>p0vHndreE(>y?|qzB<)q z<=TC~&q(_esF|9Jk-`)YXUdCxi@`(suim2C_6!g834%N=5Q!@mu+ijQX4ia5V}?pO z9$G|0H^hh(@2R9w;!O=sf^LuFV*Xo02(gr6BA$>{z;9n?97NW4*?+4_QsUN90Ph2; z0_F7$08=Cb3Ztkvs~`{3SPcE-q8O{vcppqEK>m;=7Wb03c1GNKMFWFFrnSMIj-kD zXK}dB3Am8HEYp%r-)3a>M>u;zm9hrsewU-D$W8!aQ8R?I*~v!r)T%zgb>!F+MBSV~ ziY^dd3d-FJ_}%#udocOyHIK6i4?~*Y-3%C(iaL@h@lO5;-%%x1tNo{`UI&!Y<-up2 zIeqogyBU1~Fg-BADLhLOfrZO}L9P&~x{4dD0y~-~Mi+RQ`Ffh77!go&`_O90r;`!* zRDtf4HX()(=OaNq_uriu~NpOZFI{D>dGI1{-;5LA#5-1C5WrNPLj2TLMNmd;@CAzlB#mZ!_oUM}? zbZ##={at0jN5u`(#7W2>Sp1t)Hw%~BT;6G_j8m|gjgJD(A&q)wEhm1b;$#16khqr1 z$}Y~a7v1W)LvPG2te-STQmT#DXw}thD%@M^hmHPpgDi zHEaFzoi~LCF4fEbnz8P{rGLE2sFlj;vtQ*~rYv;U%~E6~**s|~_&eejCt-s!H5^1s z?w?1H%;p^*1me_WCO{komy-*m8o&Z24C(M>vu;cl|!>!P+4Z1;gSxP)R0=B=ym>426usAnV zWI4zMz4Tv3Sq&5AY>8&fOH{;CT z%mkmVw9^2yiktDBn3I@{{Q3qnQ-y{dMuY(14|VQTF@{k zAEYR}Hl%8{c?98ytk)|UCkhN0gg(@dK&w6!7AXxCDQ0Zg-&THU=5#`jG~kv#`lU6k zcHsSl^XL!TF0E(U`j%5|RlAVi#o&_M(~TW>bi6TU${Ev6dTY2z03=B}%tAX*4`v0L zfmFwLo>~+Dx?*-?Jy8F$E17wr1DUBe@lZ@C%eLEx7FwhCw+LACF8h4BQ^d|2j{1mB z9>rWyyH?3_xbCkdbUD8x*Yxk^%T%brPs9B}8ad!`D?8M)2A~iRAx?gc1_6>Tuwt@Q zX8{xUeoC6m4w}=;>Us|r%sUHb-?5e)S-p;`=2k^)_blJv^#8@KO8O+9A#BxBu_=#F}>YYbxIPOq|2Pn6F3AdX%S=aw~*dWz!$y zWDqRs9q0iYY0Fh(g}&4kZe(-;F`XQ^g)y>FH{94~za2Fm5EQRfl?goaV5QyG*okls zA=*11CB%emV%VZfrA77YR(3zF<-|e*-=itn9NpIHtN8*G2{K7ylC8b#PF;kJlD#JZ zSgarMaw{?r))B#*zrmWB*idCAQ{qXQn~?kAFz$Tjf&`Z{cLd=YJbk?+{IFt+QcqRrVx>$JI$>G4B6cY>;`DTociXVQ zY-CWVO>>K7AOjt$IJWT@CIw|~YlP)Tq80v?{9W}E*vVXXu#i0IMBcQW2-A5C&+1ue za{TQ+t=~WhPb$|PX&|C&K)eEkJJHH^D@5R?Xh#L8pE~>|c_IKc+I76-) zNiLc7;`%!U5MK7F4mp$pE?DEC-^JOK6`ygu+mo$_p3jxrrmTY^)rlDc6a)GFmt2*c zff=9L)CI}1DF9Byvqa;Fs7`u&{MnhD7Mew9ygw_YXlp#q`ecgLVSz}E>g;h%bw0tZ>t9mFvIvf@AF z)$5JaIPkopHTGH)`xo-+AtOb&`ssAa-8uZX+KSpy!z#0_K26Yj!@E8&iIH8PN6@;! z`t@O@9iskZjW+3am`WjjP-6lGg5nIrTFo1+h~Ji5fCiMo-M(a8BRT2b#`4uPymfrTKHW)Ggh>VP{@?+>mOq74Qx%Xpk4SPBh-k*Bmp?i$z+$6L}Y(a_g-l58H15 z9*RwBe=S3+*x;;0^+Ou#!q)hvEh2|cB}1u&R(7kl7;-bHbR(ORrg-JksQ`5Sb{0xo z1nlx%9x@TMtzWZ_Gwn|H>p_jNh52*ThWbqiR8ccXfF7M;Fa z`&v~=Jt2^#vf?1Ot%=fs&v)X(G66$ z0BpcrWl8H>jK}}?Pn4s_n#cZUSaI$q91jkgWDYl`T`l^!Hh>95=8%6U7-He94ecv1 z?VC%#_j+yVv*qM6A>k#@%k}h}qMGMFojjIqFTYpH?It+TrrS=4(PL2sXNfhIyEZ&4 z%-es0SC4&H^j5UJxHo=YiEoKq=iYE1%$u$@nNB_ZR}_}ms3lWn#c)u8cna8!e!CbV z)?b@WV(Kr}y|%N+A{gfY-I4oA%u=T#cK0+Io{HHWeY!Kf>>KA)%Iz#d9@F3@&t8(I zJ*{XH!6&!Y-llTLTjn_JXor)Z*wR7PaTed}oqnX|Ptvb31c`-k)_irnVsD$VoG?m8 zdb|wEpqKeYfU+x{KJ_=pi5WL}l1$Vf4_1&R zw`5TR9Z1YzTQt3SXGGbn;FI6$^nE~6Z1gZbWCLzQa6h;kl!`QGIR~zwI39-^G*fEpv-RG!r=U9^ z{wVtT_uboQJ^KHiE1ruwRW|(Hu#7jI9HaQt^9SwMWd4Pn!>Lr`#(%h*Bs6)f_y$Y* zw*?;;ZH8^r=dIvGB4MZB>(lYt#e)%1$T8Zkmtl!?v`9Xb8ospumpaSp!pPgMEj@=O!a5&^$lm_?5DTnIVw8Lhh`w8AL2s<%aA#CO(@T>sUFm)rgEH%3<+Xf1wL z3SY;Pf5-Nx_R2Y8X&qm(l#%*3Lu^E>5@`9{q4#L743(M%~ zOhm=Jd3+k%GVAMGCKfY&eT^?~cS{M9>446n#*xW#4A{ghx))?vOBu9VR84iSDRx&* zdM_flqvbM0{Xj||@3Giwtd}``dktwiLez=|;IqWJ>nHOrXzc}!HljjBRu9O*DSO)u zT6)o;Wx+AEc&TXd_g&q=x9RWI_-j6b6!MnnzwG}HU*{N`+Y_$+w!7`Wwr$(Cy=&Xt zwQaXgZ9cVao4dAcTW6n-Z%*NuPjC6E+gNDvw~atL5E!-As2`j!DaSRP{VzJ**O{4$wAkqr z4Z2Ff#Cxp;8ekZ=9K=k@)>EDtB!vEgsvHT330?GOKa%{fvisQisaAg}kCw)s-N2`A z*6l{R@nia_GwsN+is#2LR>I}^$))uci&~;VTGM0igEXWb92CULD0}6U!$=%2(i#mw zf-DUK+II{dqW+61$)XSHg2@R@L9`gMd`PO}?^mDgh2Yx5;KPTR_e*ZqQ`>i2_vMlKwbR)Qcmjg|Mnih}<6 zqk@Xa;c)=9rcXU`b5+7tRHKJz9gm@~6od`qQS18A@WB;^VqRi|f^ZCreI~nEI=LCNszu3I-n)FV_<#|46g?xaglLEcG-7d?{0KK{`n=`(1u5u(b3;cWT!~WcS%U;(b3~b&=iYp zf+U+jU)OtP2R$GicI=Jw$x+rMi*`IFCi>-u&AGsk zYCsh?On!Ew&Hq|0;8rudtxGNjXal+`C!%>RFh(cW%r-&ok1^f151Y4-FWU13QO_hS z?pJ7Pw)Ijpz8@@!c=cA%9qV+I&Lim{8H~#l3JmWi0#bC@kiBCHSFFu^buNqE^U@Yg zhOdwZxQE{5;O8)51x_2Gg_^1Ft@JxHr17*@bv~ zN)Y^>xL=WX-_T`5S0^ZdDNYPI;Ha{K?t6C&A?!RM1A&4#^tbzKhwabWB5KLk0_m>M zH+3_vx%akOW$auOT21w zHx-(%$|$=Gp;9iRuWyWR;Kcm1q!dQq0pN2q9rJ7Gc#983o1_Twd!1)RM}&n(qlTYr zglgo~2$$|bU;VuO8#)#%0Vd1t*B^yS6J1ESzRNr?z$Mfjl$JKmGn!JE%keXvFdUDTU3d}hgXhYUh~iWns4lTKPiFw=G*5} zQ~>g^1Skfbv}QfvEB)Ksh8q4y+h|2#^T;I5Xj>>UB^$pxBDnF!+t&bDKG zrdh}eoC0Owd>qb+PIke0=&FcNAKLc6$Ru(bP^EiskIbHHnOZ}~t)@}32Y)?P-*1~h zvY&Rl%B_cY={(U4vxctpFm_Vm$ZQ<_;5B6aZ$s=oodFQo13&$t4f!=lvF`+1iPFV_ z7_(e7Jx8R8C5{YZz}9%J_n&`V_8~EM=DnaUCdoBGxd%U#MpRXb#8JAH%UD~ z50_|$4CM*4^1@ngZE@Ai-J*$dfaKVzqXM0HlNkUh2)&blpXG(uwE#KJZZHk5V}W3u zNR}Jby*L}6Qxe0ce~C9}67(3@;qttD?RE$i8!uzM(l1dH!lFivfhlb<2s67wp87_9 zg^tDgZYrl=q>~0=YUWYrVfjWNvYjId2{qHyg6(TVrM>@GM)sdtEEe%ZX*N~k1}wtY zWB0hNAon2!FL~!rx>=&~9PmUeKX7GO`z^9hm#^w==TkWDj5jy$xs1#UHy-@l=4l6T^4yaB$fP@A{ZFiD^V;)SnAVflUqUSO7o%B||0Mxg#tXBT78{0*TIiZa2*R#*YIiu z-IYTnskNkZDtAt2q7pzZ)j}tnhv9J5zF%yY@(GLdDdm=j@|z6Hu0SgsqWCF@?V3dD z_bHy0S^V(pA4PoM?m1_29j%AkE90zgu9Tt*(+wm^9Vq2}0P&|0gjhR#`n^$%fb2X; zmou>qRw|l%UP!s1zsH4`f?t0Aylu_*@sdB{@y7X#JGs8NGWkPTi)4dXT|ewRi{jjg z;R2ogs zw1Pty8zLoRpAXM*O>RFsSNi`%4hVvhLyS&8fz^Ue_Va2BgCA*j<{L-$o#VE&0yfkd zuS9UmZ}NpyiF{&x8jkJ$I7r?nr&{#5@H)X?qU4q@(TmLp{mAeV6U6+J z$!D*2v~aFPuU^ZMg{#f7&S*N0<@zbMxN?s={~cBZni*5d*f5zy_|JWfV45OhBZHCK zCwh#4DQ6Zkh(D;6$Zo(|%g)Z0-D(AHb%j>9P3Fs_A_>3^6ebdd)zpsmzIcLCm;j^$ zxnEAm7fOa0UX+PlVNzq!s3EO4ogFJP$ZKxoYaeI6(C_{UY?{BScf*kBWeHyI$R{#X zZ0ELwzkY1;De{ExuL8z4?6{``B&#`c6(Xfto| zWA4Be`CM&{@;UBMU2tYSC~kN9SJo8fHzcT$Z$Pq2iZJcfGr2%!9xWYGmmp7G)Avs> z8R^ZaoT`xFrV8U6LEm?4hFi%3f_-@=u(_-|ZzvZK*`@m0uW1B8V&rIE$t1i8!~B@BPp zVNnvX4H!Q%qY&s8*zUn3-)eFL_^axM$ep)rt5+s`N4pzi(Z1j-OrdxWEyk2%FiJ5joaQ~B)5eFf-m^9R{CuYZ1V2PRntW13*dUEWr2#pWF|rb&>*s`;>X<3Ey)ydlZk<(W(iIJqA)#acxW*~4c;tzzR-9w!aM?YY!5 zHq&u6t_l(*Q!~ey4b?kK9WgBw?yuA`(;FidZUA!4gua7RoS?U!*G-LD%4MVBvPXrV z6>D2vd;KL+lN3X$qF5ef1~98GQmMhg5M`U)_-II@!8?xUk1p2>u<)i$rNT6i#E&aT zkKiX7w0DDg1eNzPAiQukI20c07`yIsYBnaw7)#Z|_?z_Vj91flL9IgO8M?)7Aj0zQ zjqVWW$A(XwZMv2HH}4&w@rXmjLZzhDkRL?0a2L+#e}vphxJ^G+)$XQa_bCWD8E4z7 zjAg=|O00IL8D|FG(X%^jbbcRZxF+xWE31ykuoy2Lm7kt(>s$`8t4Fm+=9#(9gro^g zkurCl3$H$4EhwC5gar|nBwCO*qsRdQrx4JcuAy|mSS0v4{ZP$>wyaHfWN`LXW?Er- zfh6rZG3=${oX<#|`ZX?+?sPE!A#&Wfyjwf~p}Ar*haRFPE}dHJt*+5&hK;VB*>N#b z0q_Nv&;86H!QdwAgn_jmz=MY1(IYX=I8qMu)1No>L*-B`*vv#v->ok!glHC5tQy1` z{~XaUL(5|=`}gf&=r|jtc*M5_PaTcYWp1eZFM;+5>DTDzo)yFX@YhACm0YiNA zE(*2(CKA+1EO=EIqTt7d(R;+VEV&I{Y@bhX@UT>dlEzMsq~r@;S$D03uRvfNBel_5 z$fE*g2xPlI3!vFUAxA{!h_X7ov4d}Ak#CK%AFow3%M4ng_c4;-?V79jR&)Fa6&l5r z`&=BLC^{C$aA1YiWk8d0Gz=-V@`y!^^ErF1SE1y42E*oa=R)oO;!prdjW?y+`89XO za5#aYI zLr5Tkwh@{1%dz7+Kc$55ub^D9NZMMxc;KvYAn9v>TQi-$Zb5N_scfEYv~s2Wa$PXo zrDJCi$+Z|N#mUK(i$0&uq6m?D->c6e^A+wU6SBI4gys+<`)Z22$sS(d&gcQQk9Yh# zj!w3KgX15E&c8`5t!1iBtro}5s=u2>cvh)S@n`kdxodZPV0PBKg4zat00;g4i@HFs zAt59M8CYWtf;=9Hm9jR#4{XIF!o?4ZpbKYqZ4aAu`e{6SfuwG-U;EwFl+ytLeMj0;;3MQsJ-_i z2^6EMRn4_4U&$#i?{dIb`CsXD4NaHrkfi|40e%Xs~*wS|I60z z7&s@e2S5>(miT3mlQKkORiL-TQ@VcR%mjnFRQ#=;nQO&!J5f}E*kK&w0gWB4OHLHO@hyydLRm)k_ z@*FK*oVy+fS_HoA6~SCfuP<|X4wimx%!F)wWEy-!bB{|j^E4x_5=nVJ8SW?&M+IsM z82qUuI;-us?Kx#8nRSnP6Cni#P`mQz(V*QAkNBfk-ZLq$G(dcAZRAy`>t=wx9W%de zK9_FWKKzmyO~lq*P%4qhgw>)aT3Uq+BMtnga4^y*rfq=f(Phy3G}=u@Zh_kdwrc;H zAmKC7XkHWy`83LeN(#e`zTXHBMS+p>Mesq@Pu;f8DFRp89;!kZQCi*bU_@CTbyzqT zUTCIG2PI0Iu)#tb|Hx}@ma2m-ZoV8gE;ne={J4PwzRtk6>e(zUS$Q;&Q}LBVV0vP7 z$Rcfurkf~eU(c3pz3NkcX0(y4BczWeOYt0CI?U;nEuY4Q2Gp2qqrT@3y^$1vCR$q|x2L3E-Zbgugz*2KfN0l?a7GOUnxW^K^=l+yGh1~?^q zH_OsL>yY}V<_@uG>T;qE1jr;k!u=pXMGP{iJ#tCo>LMC&J{5XUBQNCR%zL$uOZ|iW zc$HW{HqYEjILLH~Emc|C5b|XP+h%#k>W2Wt>a|dVrvxzP#HEQAh>^nqS<)-xLy2@s zb45t?U`~HiWK3}NbCoW#d8i-OX)dhEpAKU3;_UB422J};(dT@1o0>5xAP-04o$T||R+qCQ9Qa-8Y!n6M zg0pBCuG^e13uWf|@C95+kKLO;Nbyr!7>VB55@&2)ThKLkJUSmi2+d9pMHARs$vi!f z@gBNFPm4lsrd>GNlXTa0GZ_zzW^{-MFm4^_8uUMntgj16jMNF`00UF0P@xdhJH(u6slefij#u z>T0#qd1sXRFPDn(ZT|kUCYi?U@MiwN#WFg9px4>+%!KwV_7S(7?aG#01e^)Dn4KR zW@*s!SuJfDH%f^jx+b(;0=ej-jF&*cFDFI0K#%2~;a;|OCabFC<#Do|Nu5f)VyiUn zXUsBd)XOyGIsrvzd^(*+t||^3jU>`%y@R5S=(JoG@LsP7amQwh7(>TFrL&Rk9*i_P zD^*MKsx2bvfyIFwpn!28DMH2dY!F(An{pY8JLeVVICxn$oE`-9`}&OgrOY=)kfH`p zW~#Ey;K@8y=wM>YIVcG!f@Y-gWahKbfJP=?1rH&b=0H!Ep+T$RiiwJZr1z>Xpdja~ z-Bt8u?K&5-6?;b53W>@FS_w;($h-94id)+)<5__QTwg<1%>EHE1>P-*aYz#wfXHyF zXVc}&n+7|_qU*4MoVFx*6}d`zdsiH}3;cC>`WAU&u{Mu`hw+5C!#|$rev}@IgRcW| zkCaBBZJro=_Zjhv;wwz4j(_dx0!#YH`F5HiyuSaUjm-69^rPTNkoE?m+i)>UO`=bX z#R<@&s7vY`5@=Q4pLgMXyjFVb&x%L(RUmGYl*e2-kO$kt(W;>$q(Fj6NJ3z*agzwu z9gH|oKb7TzH@w=hIq_zI)V%9b{&z$;CJ^R*sLS4S1X>p}1<9_sC7yP)RVr`nC%jES z7H&@@ONr(r)WJM1Aj?V#KDDfV2DGa3GzUs3A7?k~@C%QuVH@)($RF))>j& zND%+tbt*z9D+hTEnnSJk+`jxi-N}nfWU!e-^<5?tj_rD4@L9%(VE2(<3N?eRMU90;s)p%ldt7zhK|tVx z%XMsXC#kE^>gV$= ze?Jy(LF8~gV2jQ*Dks-%I4#7&_QeO(x)U?L1 zR449EL`33TV`0hK$#xyD_csVz!=#ou%0IW_PL`aWSoCGa=@X-{ViTc+`5!W}Lp8Bz z=_LrnrQ*SYfm3QlXep-qBRP`!KLOylAc0*^b7!?{P-aih=1d{)uO*k}o^;MM3$}7N z3`W(zLZ30dQ|m^K=}-@bRd`awdlA3iiw@R9LB@0JmuiEw9xS zxBYKDlN#t6n^M#D?MaH%ye7Jyt#~i+8D*fEWRFxbQxo>MWJcN_3NZT)ju3a6*VI0` z&JbO)kIG*h44lPA3cf8&%^8JLfP(rD{4AMz^vG_^*v?DgO=3U8@Y30z+h45!(CJh8 zY9WAS#w-E$sp=v&qe72X}v!i6)Q_NjSC*fmNHR=;v#8fHyC3tXN&-R6YccK-VMi7Z!KXA@1gp-qXX38lkt zq6}RXhfJZ@c1O#c;WI~ED4aQI?Rdl|eL=F*%lDE_po|`IIoDQYj*!7(I zM?IIX(5kO$kV)kH|HdqLK&+q-k3zaa;VX|1E&se4dNw&8+$fc+TBme)9{5Ao-GWQl z`hi+5JqS4QF;eng8MwJjj`5^OHfkd_y1ijG6n&tR6FKZ8>-BfJWB)eS1uo$6EQ5Un zfjZUtUV~wP*RPB}!IK|KVg+d%RnixfFaZC0gzO>tOr|q%82a*=H94fOd*>be$F>g_ zIt%_<9b)*UVJh-cTO=w-TvRO_-H6e?6@B{_>lgdm@i=7IiBxdlg+RJM3Kd$c&{{|9f!9c=8#_Y2n8$O2WEYzA}EP z#VrvjvRvqq8mQEgo0iwQ@AT#=@q7*Y>mWk;5Fh=~RMF#2e>1Tj3rRgx^ILfGmxq@c zyFT=0>1-e~HdB{^C>6$NJ^SYQafmP<5K&$ncW*^&(C29eVkVO#@Eg z+_vU90%IFX_ydnff?uWTRK^c9)*7wI;OR1Etx-C)B+kh_8F>T~XzRS}G>@h{K)sk6s;k%7+fMEQeIJd> zYmCzSU14@7ktg^!lH*luhdf&d8)|(9;tN0)ngB6$$AmOsLKP`iOodbwt5Xki8e+a9 z5?37nigYJJyiVP5jFHX9xZTmDcH2(_AZwgj`vdNFCyAD-B5KUgKMq*c5Ot1$)p2Gg zkxo?lJA;+|)-C7;2?^S*EaQ|!8D*-Oi+x~6vJFRr z`H5^k;0!qH{_pIW;wn~szS+WC>jX94&C;ATjIsQo>+UdTU}8D=0{80pZ2+QE;<)=x z&LFiBq*C+49BlSNfYG(?<$Ar8^h7hWtBX-l#~WbePVZe5_+R+4PQ+Hn+aaG$TY#m# zz^a1&n(xVRxybRk{M9tL%J+ZU;1|EO2;vHD0v++?vw=@J|w7w^UJ^<~C%}(y5DKZ1LzyE273Jw${eWQhz^pSVJqcjs1 z>8QL27aTl=RvHnfx#tpYrV1+z3c-mxgCR@}uLf)Sv8iw(xgofS;85uX#$>-$Xc2J~ zXLgCd+m7CiG_0!zw9NlHB?m!5_6G19W(3I51_gN_!g;AX^?c0@j<8g=A_QghAu1yk zPF&l+1Ta9-j=lBmmP|}u{4lOf6T@dDzuMeOusWt%G6=F5OvuF@{K+mkN%8gJ*@Ip(boiLsM$^Ajhn&varlgElVp6Zo&Ad<#c*8P68g zoT{n+kkh#!>7kInF-G+MsvJP>9kdLd?_cQs@02zG-lo5sgk6s%TcCC9`>|`Cyc4fk zx8bAu-gT?$Ht%vE#+7buN&U_KMsXLV{r+q3M+&_#NUe-My08q5p^T_7zdE9tUb~uI zVo{>5+0g(A4LybE_hvxE)G+P`!+4eZ4xn?|fnxk|Du+HJL(YQdOwQiVo2U0*yeT(B z?s+X2IH8`!agk%e^82J~fm$Kwf&T=mb{4jA%n{S)vf+Es|1E_1$IWv@SSybWMEI`* zF1Rq#|G$Nxoc5DL3V(WC_;`$CgB935`i2X|;MC|N1c=l^Ff*mVl!N|o=z2vW_Fp=M z2}S5eckd`ofkc@~06@kfPK^u0_h=)HXL^|zKV*IC_EX@{gi4|82XBl-h2a!r4Qg2s zy25b^1^;lGg%pa}9Z~_$BQuK>RC#Y^8qc9`9h7LT)ERG#?+6n_#B@FEQnn#@*sWDl zO^p2#$)onnR%8b0vLggPV}Ymrwa-Hv?tHXRSv?l0;6w?R;-Wp={j!;8R9sai8Q?%^ z?I5c{_m1nYtFnL*iob(*9zlJHmoyD2id`#AT$V)pEge!Ldj(fq z+P04?`ZyIw2t&$R!p@~hw;TWr55_jCj6~>m@ujTnUBR0pdRvrEMuGO0UFZhQqU9+3WkDX^_r}f1 zeM>%#!M2%T%y~jUE?c{i&S1={n4oL`F;1D0rzgK-I%-&Pu*<#|Lj!jJdRlihj4A!4 z9<7=$Zx8hZ_x+>B)h>t^PBNtqG$Dzxpr@xcH=>_bX`H5o0UOtR<|&m$LHc9dpYXLs z>Tj9J_elL?u#L*%8C|uGW1D|0qgOK-X4Ipsxre5ZX!Xz$d#;;S)UTeOtLFlpDgK#L z#yNEnEi(*-$}$sD(q&k&n8AZKh16jItXag~wLvY0EzLpC#6LVwnQV`aEr*c)H0~^M zTu-sloZ^tYf5g%#to;#9Dl8;7%2HH!i@wdkq`&!3qZhH&Z ziKg?k7A~P5%0cH8i%x-3qMGV_bcP4(UJ=_~#2br3Cg=~Umio2xRnv9$CTc5%&f|aS zIFd&bJG+(JmUeIDk*EnQ+x(HLTRH{M8^$PHf}!Qp`D?EyQHjIb3@^^{)Xx%~2K_l_V?evQIo z6liamKDgXx{v~(fZJkM9=QyvX{vc_I`5{9 za%2(Dj(PsA?B;GAh%o5V{*vKVsod$bKp;iV*_ukti1bD=ro4n@rT7;Gue-vxhlCR? zyCXX_H`mXvmMpy+E4CwK3k6Q7l*LpkDQ60?Mz<`FjkOo2Sjqr5eKXq`_MFLpaLG~y z{@8LvCrSqX?LDH?uaDD;N;3^LV7Gc!z12$G3z2&3eAy-D>A9ta!#2LnGzV&#k8V6UbH6FrZL%V$}l5BU_cf0tI|+Y4O^BzRS@kURhq3 zRqE*I-P#w+eH&H9(`&}>XcdVk>R@K%IK^4d|$)!rwxoO zmDuHW@p4B^n%d)Cxh44hM;Pdhu!=Z(Q)BiesX-;MY7cuQz*BR)00pzLrV$$mBj=@N z@x05)*s>(`hSKlCM4EEIzm=|5*n`UTdl@)z=B{V|9_||yXLY(k~ zl(fqS`Vm`i+PW zishY7Wg}09N4YvLW(GI8Rc+n9T0*Ynydw?CQw(|f8FA_wlHI>Hh9!QTEh=rhVl}AX zu*?{`&uY|axW$*{CjXr5A%8znMs-}IjxN6`Ry1r-xDog_Sa@#E5}sYmlwDpNM&Cc2 zyeaBiApLl{c9C0I%DpXJC_cx`xM2FG?CfB%pS`Ds15{UPD~}&6D7+fq3}WFpkyd(f z&IrhADkVnJ+-CKeP#ZYXD6ORmYM&?`j+03>6^-OGi0y z9R+xom071(C(8jUP+>98r<^@^iwiri>xy|^=j9GQ2c?SqUyuJ@Ugh~6Y&>4I_TDWx z@O?chS}A@dUz=K|_m`gHK69YufYTBBXnAmTxN|L3*5&%-qf|Iei0i}FIW@*WH%kUnP7mwT#`LcY!yi zlh;e5ZtS%jAw#y{wc_Baj0__Q%j*P{`D-9g(BG5gyzy0t7sw z!yHU18i;}{DOU0FC2JZ7lD;c8rEYqjD#yQB6=+tI|zAV zZAnNl7)1K@u-wE;w6NyjrM9@mb(8y$RmJ`w5dlEJ7Y(H_3Yv_XL833Va8FPI!q>OZMyBxCZys^ML5oE_ysW&Q3FOs4v;r7;)y^;*BrBpf`49W2Vg3} zB(3KH;5T+)kw}8JFI0m@Jqb{~=Sfy}Qg~p{bUjy9{^GE;XX?P{CSHZjKQ<}b;AO2? zb$MJ5hy-!7Z8-J}W*FHg>eJ-mn-EUhGkb(*dm6*Z&$M*bg3r0-Ofp&PW3WB5O5*(b z-PlNMpoCe9S4mS_woVM?o3$|5KP{>d@5Wavz-R}JjL6%`fQ7mbVq`5>SyM}UT(J8X zn5XV_sCfN8kmn=7VLBf2o9J={g(_=hNMy>_x?q9$>IT(ik^Trsu1$iTY~f0y{d{U_ z+*!c=4u48j(XzNCqNw6p+ZMutl77ZtD??4);<7K#j_;IV0$ zNSj$352IPRtwO3XvXw0RiFDMw zNBGd^GMh$pYCi1iq$)MinKxnCjoireR=LVVF6^JCM4-ccF^ z@sbz%Sxp`Ri4s~iHIwO>Plpker6}`P@lpv)VvQ@kd(T9-HN2MOC$=x-sG-|H=(&2{ zSmHv-NeLE?WYlROdQ7g2Wm1U~gR7cGsZ9k)Dl;%c3Bw{hr8(Vijejeo0Av_U!Wwe7 zhnB?~D}qkB!SO1pppOD34(Hew<2}nMthTJOa0TbS^);)>3s^o?E{%BR_@|^VVHg6& zH1t6?#LfxY>rhnHNN_-&iaq8ns3YN3*ulkzc2%31*HEi>b?;?77BYU;;$m(qZ#YD* z$Ciqo=DdDJb(Z2x^m$WS7B4Lv1b0W&?i9L1NVs+}YSrg$3I75^cA(1JvYO#M@#DDC zRCyPdq|$mtB#kY{CKc45sM4dG`cyP@u7wD04I37_%&zPY=kX0)3T{lHq65ykE!G4> zG>i^OuFb)v#LK=^rG|=9(+0r47XX3-9vAkOVHpUX53+*DE(Ckqf|O z>BHAXsKRkvRgs=>CW*%#sV$OtDx-&Ec_*eq_Q_*yUni`Yw*l`{GzVtm=t!-#%zdV+ zXf63t{Oh7X>xImRlGlbUhEa|Pby=EB%hv6ly()$Q}>;6ffH?7G;C&>7dw+K zFK@H_QtPCF>GxQ20R$G!Y_9DFG<7r9@s8EfII0e1*5N~S%7f#0n`@~Zi?CX-QK#cX zf^-^Oon8!7#xgp6updUBrD11p86~f0nteSAS;>+*2Lx#RZ`+q&a*G0m9{Skd=Bsct>1UokLYTB^PBzOZoUeSV(Z_PXlJ68wit zDq8+Nz;#$y*!GLn`mkH)b(mw#pwN?K{5;-p*jY=qViv#2E>G@HU_MFOYV!^LeqgzJ zuEu4YLv%r0kmfu^HvfE{WW-O#OC?YxWV7G#ID4+%_-~7m`NdtE6M^>(!3!C(5rqFi zAt|@DcjX(&*S+*;7~W<4Dw5BzC*!;@t&Trkl@jQEt&l{P#u% z4E87E_mpEU9Y}j(|@!bDxEzJWdCHx%M zgnEDVA0Z4Pg8D!0^iX*1bhNE)+O3VpXb|*~H!^nQZXd*v4|kWd2=Wdh5vNWOx#nCOCAr>~dplqe{X<pwY+?!a=jF^1Jaa;rr6jl=>@#is!oDthC`-$_KPo9o~{&SA9Qsz&o(hMhWE`JyO zwF%S@t#9^$?Zt;E#T2vbeN_B;jraFFCH%S1`(_sPewmi)anf3-oNpMpTKJcx;J85~ zU-!_gU%Iv&>thCK5c`$L^j`!?qHS>-5H+LqeUPsXQ5I{i;MXz+-XIw*shBy{UWGc} z?u)xIo+>Acq9MDb@+40p+V~XMCU|eJ=02IXFF4>35gr`rxy7E0y3&j+U*57lu4=_R zPzAQ3+V>w_X4vZ$*?&q4>xw?;A=rk`zC}Uit~ zwyc1nfTy>+#Ym?6#Foo|aG$mlf3B>A?9<8sx_z;gb7P0MQoT~ogzisfzalMv)rGEL za{8Ob3tn5{*q;&w`H)N>ni$dKa@pvr*g4|wd<}{!ygm@3sp^xGs7ADuG+2}%*m6u@ za1nlk5vy=~T@@LUDl^^*NO?O89Q1&if$ zB^5M%Ck+e_edXfjp%gKD%>aM-f-`Ke_{q6WwFX<50ysn|*5{^~60P9STE`0=91^Az z7C8R)k{o!w_2qZ9p{NV=3p-#-r}>e}UJIQq)9xKx^njyY7=~Q9OV#l3K<*RrSebIa zMM9Um`ki^Oon!p@v&kqOcSsprB=5<|7vQi>jC=S2(TKbQd1myH05(`yO@1?fxV~)Y zi7q&vr439aq}?xG4xRRPmj<-1&pDHle8k!-9@?73bBTcxr% G&X@Yj>Dejz}S6 z0nQU`qiLxpAB~+d9CpYW_VYiNoy?x}Ult@y#X(n_22Jiwqy{gEAmxKs*@9D^1Qu+D z#-k#Btx#Sf;daPZHCV)BO@Nb^cZr#y)s6eVHW%bfyqft-%g9?71;4tn+}r@8HMNGj zFVbsOyK{KvznFUSaBOyB;oMZ|`60%Kej+k58>}!z%Q2f>NQ8`$43_^b==nhe&UAFP z@`0JhbEi4lP&VsNfxa#kl*8KhKfY(cW5a3PM~&AGh?@gfm_GB<){{>MjCGRQ>6dWL zkCE0fB)wqmsXIKFn>bQc7?G{MYHCOza6v8!&kJQ`viznD(@fXLrTJ;r$@t}8-GWyB zjwI2E{iiVwtkkk)Wba8aVD9`TvLL@1$ae@%<}fpjhOYPH>maVEPW*3WpyY^JYfbB4 z7qBay#BzR_^K+c$YokXWq6lJ9vqC(&9*(H%!h^I~HwZ8~(ibTmk1f^$JQMTH*j-zo zD?zDfy-Az%t*WgOX@p52HKJj52!Q4v!96~)q`z$Plvb8HQI6R6VD(aL-t?Xm*{_t( zb&Zu|FtnFZEZiE%B&^ZJn{z~~-*-)lOl9Z;W=?srYfCG=mRBzw#c@BT;ltuOXjQyr zHKxf~POJ;F4QGy<4D5C9c_v_+K+6qV4BM)Mr+%^>^k1G5Kw%0&;*TeB(_I36mWl}}MW$JIlabev6D zO^Q?&5nzQZ*i5zX{*^hBLB!q3ArRePuydas1Xfn1YE8F)HO|a!9XYjDxkj?wC&|Ef z!?I}k3e-=A$SoQFVN33SP!XqlHh1YyjVYA+9Y`nH$UFeX<XmO=CwV-fFXT0szd@9xp+AHz0L2(XvYBA)E||OE0QCVb zuv3Z}TY#aZk59g|$c~vA#q)!hJc`f?6|ct7UGr{12}KaYP+fpMk#t5jTz(_YKLO1B ze(ouK!EcW9Lde|ld&k|8as8ZBym=OU5*>W$@l^8n5jeP(33q=aZ=rCT$+CE_qw>f; zQPlT53dPj-IO_X-Q5J5+@-_RVr1yU5EO{`?&s+Vyhv?^`ipl%ac5u;cGZU}R|3n;2 ztb%-6(ASAb@hzp_@$_}$nh9GEugw*z&`Rq@`u64mG?t_k{ZnzaVKD1Y_~yk^fvQ=? zhjr+nC>9&YB@_*-jpX}x^y5EqrZSOwoJMiAq|nS@;YRY%WNq8@;gd!(1vSO{|4Nia z>A>#h7CcpXUtm?_MMU_+*xMX*V(#kbJpo{Mj~l1 zhy|zaqSL3TMae>JIpuoTIyW{_G18B(JoI_uD<=K{+BT{Upq?0<{dX07U?(p$$79`W z3b&LZ4_9X$;NMPV;-XfpN61iVgG6lmXQH3gr1v$Wv*-0Koz=w3w#sU-ZlOp4nv}!2 zP(uP6251@;+w2_3mi+=%L;;cX?-oCoiK0k}EP1!6CQPe#{U?;X1>Z;(RYtiS&NJ6s z)rno;4l;QUM@7E0eP)VlwjzCE$i)Jw)0XNYbFIA7I0|a1nUxi!MfA3M17*tfT01;~ zmSnTJL{P!M`AGcn`QhJ|7Ixuo>g)llmQXB(QK~fHc_e536yig)wMD6wF|hzwmTqj? zM(b5R?EP%VcZE=NCD$woeQm+kuB%l7>2M< zT3cgAxtyU+w}XBjkLfZSG$-m*&8ba;>O7ByCr#YUUbfrPwT_4nUV!&&TM2SR*t2;F zi%~kQ*yPvAsJV~OBACy9sr30~%b>1_(}k^4k7oi>$`Ao7;pnxyPogHU{G=dn${#%7Y2^nMg0T+*ES{kFh<(L!CZ zAy+3^q|DqP=oy9gV)WK9&jdJ=xzNL{aNE0B2+Gm!@{N?4YXv-mL%5r18_w4%{v&(y z7zlE5dRsYo*znjHDsBBC_brxetdGt*4^dmdFHicP&dwv#91-n@D-kk~L^=AYg?xcQiK zebtt7qYbs3UC!1y=x6xIMD3(}fRGhaM-OjP-;g?|L>hQl5MJEHj&HD;E&CUYQ2SPw z+}hN{k|ibW#N3p_bJ%W@n|56OKC+G0g@ zm1Di1bF1iXjHa7%mlRx5LUKo)0hggb$5%z{=gvxLv2$*y8dX#;S|>TRfvb}$kyHna z$zi?7MW5{Ng4KTK*ZZQ)sZeSj(1-n)lhfeeGjpgXGpCdX?aays$<30B?>*1!_H=V- zYp)yl`LTHTgK41ARal4N+1b}Jw@FhD>>qdaRqHzyX^JU;(bx-6cMXNZz^Tb$1x(ru zHzqcE%{BiMk4eUt&QevxImkNU7>l^5_fBV~qn|?B;A!yJ+_e>E%RWZZ^@}W+PE!ON zBZWE9d#T9{p4@m|a39NlMO4)#|8!u?3zKY7NZf0gP7*S<>;-Kk4l}p2SeMTO_f%0} z-rf3yQ#)sBj~nX#GKmP}X=d7T?1J$&kT!4tVmm#4J-I#HxV)$b+tj!KjO~XRu5l*4 zD*?lCscxO62c+*in_)z(ma{Nc?v?Xd)?$_>qFG&%uAcbj6*qp}P3z}0IZgmBA(h=t z8ZK+ub$K~j*IfWp2yA$#-Gs5*?Q0KMDLzOyp1@R zHAFpFbJvZb|5N>O6c-K`F(W>hfNXJ-uwDTNaV6u)KW&X{jkM%>GvWO-HcilxIX5t& zFYoVDGV}UQoap`_ny~rY$M+T_ER%V0*`~^GWh_FypXRG zmaLG7P?>242a1ohyiPoa6CoV8OU8hF5X&9+)#tbWwF#nvK>VoE(Y8L>R;Y|9Zx?~6 zZR!XXbKZ6-q?{xXk~b3uf^_#08r zFCvd2Y;($6-X$k6IcO;h9f)xxgfUyu59EE4%C6cit%zGsEE7FM#?hHDpO&` znUlMxZpWs4hc7Ahh}k-!;=~(MZxqcgmZMuwm)b&Gj}cHPD$|L~fX&3NN6RfEO*WaZvX#Wgwq(lAm%hrB@A`HFw?ePJ!U z;kx6CHQbfw4f`gyoZ#~0Pl>HlFPiHjhNqh%R|zn#TJB5V$E3r52)YJ~FpM`<9|NF} zUOX>NHGuW!;BwKMdltx=RE6S{bQJt6fNds`95)I^UzTCp`IuwJ0H=SnGj;jEr6$=s z1>7&(Btm@dL(A6eMV@tOix`h~e1S!jA0hd6$-P1|eiOhc+6_XM6 zVQeoxEByI)h&{q3t%I z+$j4>VsGeDL}o?2ozwQEnbxT*BOHj$Vc#k@S9>z)D0n`fwN+X9Oz(}*&6 zg5SIDtx}`HsZiS_*J?#W^!61f8psbM={4v=oi2@Ta0pz&D>{qU*tu^9koti zl-?`Jd>LtKz5z6aMg6cgr0Mc{wi2gZ8QnbzDUM8MONm5BP?q6)uCxoU@MoWz9>?|^ zfzFyO$6c%kA59!DK1c0z4etmw5Vt*A-pDjFDbD|8&K{4C7mS3R*`IDA#2*uzG$E}{ z$g}WQpE-Zfgn0jVe(*^g9Jcen`V9lRt%u+~$D2WZ8!{ua zu~{c$zL82z1`7g4iW1{v5Nu04^QKoPNdzXUiaYZzItfC2`|YAwRYNl}L*&6I330~+ z6K2UqzESOa`MSY?QcMx6KO0-=KY$(;oq(VHE&LEg2VoZaA>rxs zgpe<(v}{0tHp%#2{#HyNsd3XE%n_~XEOK4DwImMtHD$AGIP)q_6e>nplUmGE1dR8a zQi0v1KTQp_1p0GwDR)$DOmYB+YF8C2f{85%7%VO}v4m{&zm%%hUYQsK4!sNOB2SYF@;a%&wUAE-(ef%2Y#KpYeiwwoEcf6vHTwHryiT6LGIex&vzKy}9)zYZ)VCy&FIGxDBVLciqi8 za|wtu=M5&;lImRDqWA-DhVwE%Ho8c?_Xm6WqPgt54-1xAOs>Y4jjP;6nqOCYav7Mr zyt-hr*-)dNKDW|Kxxadb9E^huJ%^KD99RY{sZ-b*kQZJ%A%VDjf8ervjZwLuTz~y< zP;?sTonY?s2Fo11_OKL6@)yR6mEg0Mq#m>(R@(==edP=P0v-g{Hif00d2e$SXyvJO z36F*{@)Drxlo_S%isKVVifN&vUV@*CuCfk$NNP;f(*m}S#K1t&{ki?Ub(5}xvrr7& z<)N-MN>i7~ZE6ikz=oRk1=#tG0U^2)x(P097CQtIX9O;Y<^I6Np0H17mj@0#EffM~ za;nUUFOiRn1}mJTDL_ae4w9sXGdInW4VoJ#33}(K08*0xb~@Vd`f*&!%x(K@D0>Aw zn+a8dE6*j~Q;Cf`$n^djV2o_#bi!_>7$QwlvyciO-P{^|Eujhzv5#__iVPX7(AGXX z7Lup{Q>Jzm?^na~D3n?gEbi`aGSlRU(3y&Ts7z7EUu57~P=UXDOQG&E!P}eS6CzL> zr6Lz+ou>md8HA7Iz3?f2iK2JH>~4=b1oT&cm%+c0jKC5ejx?oOx9_$_G)rl(q7rnP zH1aZw*IZz#?qOn5Lh4gc$~o)v0_mLeXr>y>8(*=fEOnooH$2RknuVk%US;agHd8GMhG!)?cdImd6c<*>)JPC7t}+dmkWbDDW)$<)maqZ4@tpkKY1pKyFvBgGoU zldRyB)G2;IwO?v?WExtaYL5z1fet$ZejE=Aek@=c3s$GdNtaM;GE|8nvFj za^!@vXm>cq#K7Qjh&UnXSmY&<=gI0jd76QdV#Y!%(p1DlSb{)nSm#06?Dsj(R;8~F+4W_PQ@ekeF3daxe2d& z2RZ4XxN;4xqwfSEaZ=v5WPHUEoC=^+W*F>&iP=gWBz;b1nsV9(S=u)Ndt%GWoH5mB zOR37PZdrb5XPZNU@VcECZGKohkl1(wBY>TZ^;K<}rmxuVks_J2@nzl#PS>e-a3t^@HEl zb@Th|j%E&2@ZX!pxh4v>Ry!-wuf&I1e2l+0(5K!<6Z++Ku54XHQwE>r4T6z;1&xC) z7=i+0ljh#L4W~*@;OsDqp%!_|)+t;6;S=6Oo@cTW0~)RAd^y1!{M_UC(eKW)8>nN% zWtw!*!fjZVjgRoO`qfSKq#R}TAnHqPBfE`C zO4L6Gn;hMWI#_-jEXZapCpN})ySgZDt>V(CU`v9*!}&(<2Bt*r@9!^g`qD_SZyEs# z#ldIgR$kqt&rQ19o^~Kh4fm4Bi~lapW&3?c4wx{Ptu`RY$S%cN9Hbq}x2!8_mupKZ zOKeVU1t759SEn>M8Qy9apo{e3f{TP1X_|t!wUp&{7usC9>vt4V!`UWLTak=}+B_S! z^YZeumDL#Zb$w+KY3^IpxGN+l z_lSVm(t``FK4gD`1K$ahOmC-+5vR30a65ES&Dqp$p+4=(%&Spn9v98}18k1>L!x9ov>ydAZ59si-CJs9yJPlhsRfxh!V~ z2I_QRbgDA|L&?d>?pnB{J^mpFQ~xwaa!N-P(bMid7!7{xOvz(%qpRUAMub#0h)3+1 z=vf5h#LUnG71S_k<$^z`&UbY;wzk>*Z==bpVT;Tf--+vFpTe?4*45rgGdLB5;V!A(e)Vu<&PneMj zkbPqP-qH?A{5`)MKDzx=(8{GD6Rd{UHju+*?_;bI)_PjT@BH+R40QbP<822!4>0^5 z4D@|>2nY5A2eN~QF?@{JgE}2=Lv^_9L0;PjvG`o}?-AXzcI^DJLkL_39@_)h>3!H1 zF0dP&VuBYP&qa{okS;4SQys%z1#{<5izs#*Z!?TuM7{;s>ph#T_raG*4?1^%@r`Ka zPKPZ0Jf_zI(uLFN@+VT~J6wKI=DQmOeP4)dIu;YNY;FoqjbXorT<;HWwYkkp-&5EY zknz<6O84p0=7h0%;HJ;T%4I4RCR~sI>3$J2OxW37==gutSz_zqqm=ip#Re|Ft66WL z_v?yzp07Kz-p5&k;1l>p04ZONO<#|-H<_~`S*-st_E z_){*30NS0R=jA^6c&+Rd#6%iW7F)9Qduy1G67{sFFRcMkwo;e zhKQgNV^L#vB4p)iMep26#{N9&?|ls*Y17Z1jxX?HCi^2?ptqUA&xJ)RD401lj&ghnb)?g`^^gZLsfu`F+Jy|<^;3V&(hv2 zYngb%AkdN^Wl;B6<_tpib_1WV+OcuX608S*5aOw;43?g&Qe}?mU=)z%P^Df2t?hgs zoW5wY2tft@vteaC3brE^Q3E$xgyr!jn2SM}!vcF5p^W3=_?|nCi8W9H%!??@av#UA zso@B}9h}9M?{KmSCV32e%tnCsdz86&z}uwPEBz7 zK%#qC2VU*g*EKCsaeK1@#_Cy7Uf@XS`DlSe3G{d)lj6Z&-ogyXQ#axw{ALewh9iOn zp?aifd<$?=jLzmGx^IUcVoxhnLo${RUNBM{dx7C9oY2ro1)NhFKgIP4n4l+(c@YHQ z?EoaU@K8)|8Q}^p&z05rRbBJYL%g zRdE9gWQXDD&L(VW)0nmI`K?4A4asT6_9U#+vx-mt_K|u|O7g|5&_1nG5*vowSU=_} zEcz!BEUq14MMVY7rmredY}U-X95pvn74Axki$wG+h?j==CI<~(X$udvZB{e(=#-lX z|5dN@kOCArn&nW!GvNd7q@aV8o>P0s&9#~Ya#YF@Ui8>{JZVn8k!Qah8KtjLJ3lS$ zQL_DJ#fGLywN=n-Z*O)gFrrzuq`TqglutcL2E;8Maf}}~`(T`qLS_4W6`3Gk8i%vu z0m;>Ubz)?&sDcpnLyj2hHjjorp6=Bp$%O$+SET+Td=uQL1tsydg}0NJ(Cd9lA>+>v zLF(21(KHVgN+7k!`ibx5%+}QFsfc|0iPr)yse*3vsF^2 zoU+4VB~-uE?YDuIQMP)QUzOuB_oXwZx|6mA znsHVzXtx__RV7(vJ^vD@hfT8( z=vp7k`;%i>r?W zoizm>&x`41pc9Ir)%{pyBu;RRv5AbuJA?pN7S1{x_0yew*;8K$#%NpRyc+3w{gyxu0CEvW>Bm|G5Yra#nCjU<52y2 z64sG#E1u5IM};fd(P}~|oEbe9oXgJ~R~V4s&Z}%bMs?CzVaeZTSFdDb$x)1ZDot2# zYIpEs#n9{+K$8qqNj*5mm^`q-9D4;2ms{b}lVm}5-1*24&U>ag)>ggvM07~1$J%1G zI?0`d^>cuEQ)w^o9jtdatt#k+koKdVhEV}HF_8;bKUTUp1p@8D$4p>E1}uJAyC$0a zR;FD@Z0(|1rU&m3!IsPr0_?J#F4T&{t1MW#P8y*F;Xg`Wo%cFruCg|w+182{;`K;x z3R`MF6|!li04mq42hl8DDzXZp#0k^DTqLUrA2zkb``&-*B)N))4=&7`#UMg?Z2$4ZpH1PO z4gr@Rj1ZKn;MPPgFxQ54k%lID!T1MO{oVAWhN?7&cv=RXO%w-9(x|5S4;mqrJUk@J z>Esgd=@4@G{mx{4)@|iCLcHh;513)bhiD>a)@ForYZ$zQ&Fx|;VoHs`g5j_m==s3k z`$VI$>|@RU^?>U*+8C1mi zu}}MwOS{~|CU&=9QI>mI{)wgM#_|dJybG^aeo3w1Sv@LawSMDz`PdqJdY;?5`_Gz* z_x=I>T^?T!JP4^7c&M#cDj=wa>#OR@Y{_NnI{@&I$Xk)i7zZn3f;NRh{ z-W@NrY`SMpPG7IZHx44<)MF6W3wl2_JbF9xB2a=6q`E4n$h{>4x|P!`+p zgz6CV+Y3Q3RqSJX3inzIIj$SP%0Bg6^!I3~2&V4IgV+|nwEl^kSXaB~{<=Y4Vvyns zVnc-${^v4-I0RcICaPcVQcWoR7CZbNsJ&U$iI834DL)%?98F{ z8+16g)NGO~qfpCo)aHO4^lQ<@cg=*bJ1K-ga3*j8-D+zpW8x{^&|hWNuuW zp+Itr?0TMw&25L}(^F>GvB>vJ#{h8xpUYmf&-~(pT5Z8=__JsjngUzYyVDW8rGn)#gCO*Fzfv+J94iuqbUSsFt_wHWh;OF3o%r}b` zi$?=1!54_et(H2F(cRqYTjPu}QP3#1Wb*)<-o43uryo>(!fbDIMiS@}8|zlFZ2n4K znt`|}MOh){bQpY)!7;}PVweEnBR>3xq9JFOIv*^9ho2A*aSOFBy>Qa3lE@a#6`eDvdS@5y@YQ7P%vY+-#J?pdHx(I9spU#Gw&a>{Zk);0%Eg zjGsLCWkL9xJaM|yBD4Zk=alaN!^^c)7! zQF#kn9|RQ8ZDZ9~ZtFMByRMkgsR;tw*Tr?WR&-4$5O@U!9B?gtQLaSa5JUb+f9}Ec zd7^H+U}d~XL+c5XV?KesOW%)KUrmjn_;^->6%ZOhzo$|b_X{)nsDHJVa( zfdxOSRNHIaZ03_gYH2-&7m+bNS=W3aQsW@jfSfi-RP|-GwU=I!yl~KDoE`J_K2$lF zcs>brTQtn{S_r>a4n+^h4M=#W1ceciD86lwiX`2fk&>^BMoUbP+Pf$;>{eOquC<>& zK1dHSb4MkwjPvPwtQTw+!NVxsw)bzSx0d|(iD(S8}jU*<~r~`cD_2$^-zIg12X&-6m){MAnhZ2G;tu54wAE2 zB7Ion_=kYTg@xoe_>aH3W|GfZQZ7_+zmm+H<=`1ct^=fgN^9KdZeZ#H@J^gZUiQWj zFf0<`0ba1^gke}80nw`jmg!MUTS)UsK^q>dwkpV-dVd56vO^NIu8MSib<&%Zy9L_# z%}dJBz?t}|lMt?CR5oKnuR&RgFd`7{tHB4=L|SX-cT)MYYxNM_mAP@NyNB2n;Lt{= zYQtg``Fds7j96t(jyzJz=Wsn&)%-BmGW~sGW9XipiH!84QUVB(@@xg5sc0&XLZ^#{ zJwHs7<%Q1iIcGDqujK@&>`8Qd?cFmS#>C&198P|BHgtgv$$+qW{tO` zzSV`a=J&jpOVvZ+;Q*3_TsTsh(3Eb(EH>F^m=M0(D7esW?V@xL;ucsm47fPn^?gwJ0tE1F*94}J@s6FcNBK1(Ku zM?I;W{x7NTBqgNc7tc3+t!`ZUhtlS+6yfsEoP_n?_JZR17VV%b?tgYFO4y(k#qkc$Luw^e_ zFgQFKjTK8&kcbz_6q+6^q`(&uQHDY28e|-d!s(-pua5hsXapU<783n5a7gs)Ln!)d zLliZ5oXyjp3O2O54-c&~W}XZN4rZ4@q9=#*J2{}E;|E3dT>6tJcPJiEZ-QraKN)!? z6OI7Anh*h(6vcJ#-r^hLclB04X;~$~MnJ3b^ly?%^qqZ#nG)?@4@p4}+QY&Peg7Ii zfm{P&sU~ng_Txn7WBB~~b~>{3^-FH9Ug2X(HAv+C2rXswYDlUUcB4U_XA?CKJv}UG z=H&5`C-ZY7#O${O{$k*`aqjKFZ4H!KbVGZf^_;ilt~%GgqEXbajcbHVxNIV;SQD)c z04;3|?@IKQyUED%F3O12{Byx2&iI<9_BjM8T-n90r(M^(Yu;67Z`-IKG!9hUhx>Tsij6-M_^{-gTZpPnJi+vxQND8EvcReDgOT(!+ExN#%mQQ?HCN!i3Ht+P9Q@7E_# zm$PB9>_6FdFV;=uo%tR59lD~w`-=@ucGILkNiTk6xeA4?RgrLn{}mE4f?esAU^^I= zlA&$-*O{pm_+HYeTwlz8M%uC8t_(;jn&wCf4oTEKL5a?)7j=QI!5gn6Zll{y!wo_A zg|{^6X2C@BMl|J4pqOm9yeO>1#B|XYgK?UD>t5EAcNn(mX0v*s1H(BX#U2;xsY}`= zw!McYre|e%KH@U3yqtX1vj?yz<;?W3RpTC<@|m#4ON5ef`Ula) z3>_J%&R}gEwN$xJCr7Uz^IHp9+i4FcRb({DTw;6bPW~ixf){q{@sZ?g5*9oRTp1JT zBhzrb%miNEl;mzX%~R@QAZ*M*i7V`4X?z>S6uK67arG8k+}5~OSLV}ZP*w&YiD!C4 z;&eIaIu_YPfOu@v?~rs|;=eSSG{XQL2`?M#(?NY!SZJGSkUl>qq8qRB&=We)W?2>t zZ%q+!<(chy*ZXbs-SoYzBq_C||1d(H?S;o!C}b9SO~5ISRg-b~uITs8 zi#FV@54eI1@Ap$8g}voRbIKg=;iZWAnJEyW_y3qIbf(UsGRjRqTHIiXAI?v#L4kR^ zMu-^Sht4`3_$H^2OBIv>d83=is@s58Q^T7eKQ6JYbgdi=8}usC!)G$+WISDvj#78+ zPi)&}TjGl(OZlH^AFaxQ!wnW9;$E=M?W}d*H67;$g7?mR}r=)l_Go;`|vwiNnwQgQI<1&eX$ABq>hp zPjHB`>0Xxyl2H$BdM>8sW8%q^{x-zV%I*EEyIRi{m!BrI`yw-2>0Ih1Rz9j~KP9GbHj+|gmfo^_%K%eiYUN{gZS@!s7^ zUUvAyQSP?@377RH(#q$v|NGMCY0qWH+!OHgzGt@jb99}3qDaXs!cBEp*O(7?{AcMW zr1V4d(M9mdt~Ieeq0WxreD-?6Z=k^IE~weO$Mf?->e~wOlmt48C_1_S!{L_Q@XqJ! zr*G67@#kThg2S-B=W&bQ`tlq#rOCJWpEoO-@o&(6Tqatd@BVL>D!cxEH>7KTZ9>2b zFIIitR|=x#bU*K>E19r(^4swr!6-+{PZTB8TlojUkHh0@sqnLH!B=TbPnx&yPx5Y1 zD#wg(=Wmh&O%FJCi%2psiw%H;EO{;Bvg1M&5h znW#wg0=fHPey+z>!I#<(JKIO=bNiD=>|^XRGEiZ5(NWU|3i$cJ|4lx0qeVUu7@(lO w#f?h;fdM8To~g3u2=>1d@c-YF9HLL=<_z<>I`S>k|FGw!#N&~ z)n8_&YOa>4t7p1;zUXKv<6==_As`^&s;VgHAs`^b5D*Y6Fp&Svd<%H{H$X$NDCKF3=-2*W1DDJyMP$JZG2*4pdlqCvoYen zUp5Ny5Hi+ccQE1J`k~`t!v~wt_jTa^>>;GB%o^b%tSrNfLrafEOfDtHh=oT;jgL=+ zhDt|D8slIRY@zEf%OOQe{Q(n$;T=J?n|ZCDb(gPNzA?WQ4=q0dw!6rOrV!^QC#hf! zaTzlFG%e2YAeA4!(!Cy1ycp;+et<8Ej2z@dcpC!qzG4}ZO> zmc2h%GE#54QSEtytuTPNH@Khc3p_}JZSgbD0cncHztVfdXpaZ>W~Qv(Q)y- zV`d@c<=-xnBjx1M73N$BlsWPsI&mdD^(H;_pgeM;`eLkb>_dC(O?&D^BZrTjWv#dG zMRV*$bLdHRtho{n~2fKwnZ zmlrEztcXA~8%t#raIzr&pe1*;A@`uO=(MwZuOVx-JaIfdq&*g>i;LNx6nHmSdEQrY z-kp2amV4ctdDEG9)AsG8IUV{v{kbXiqCWAaIq9}7<*_aGx+DFuG2yu-@wp@Ap(PdC zR`S#w{o0oN()#tHI_kbX;h{7AzBlK*E#;-_>r30$w~o};_VlN=%pcj2Z*6INeYNIF zQcvya``;5@Ix=3{GG96~-rCdOIx=E;d8bQL+A^X)d%Jt-DuQdW()szj3z7qk)cQMG zTM@MLnZ7qhch^6=C!GuN`RSF$qIv9mWYvQ&NeP{_nw#m@F03%eqEZz}@AV413d ztU&zHU3`l<-$HYc%^Y+x_F7LRp8bnOJJBO%3`wm5x z;Qv*krENs{5?tWUqm#!R?!47pqjfGcLyFgySsqNB z$m>4Feuva@$6e(Hb=Xg4UhKuc90MM%$vn~5RPFe@`ACvbR;zeEhHxEj z6a?qbtuMMKjisL)heOn!o~H??pp=>jdTdO{hYJbFno}JHN)XaL#7q!E0T12&M=>ID zWo6>eOd6LK@z1<)u5@Sf8@rB`A(edqhhg(%7~ z05vF2f}mpj*E4W~gse{n^V-)kq}_AKaMjJYX*nqV$hT8KjKL#9N;R3H~feD4Bd=1^$v^cbzgbZuKu; z#r!2;>02Pc>yg4&A3Ogy%(HVOx(jW9{_{NWeRPlRfpWh#SNK}uryZo05JrHeC|$fq zbw0%u&UGQ{s|7GWgZU?8S180hL{HVA)v6gNZ(s05aQ}PBmyO@Q=hU>m%fr=n-Ofe6 zV;X;O*X@3ehB@yJD!qGLF-;rwKHo`MASg`MxJ-k!{rzkN%;#J2A3RUpK3C)sZ+2&W zDSJ|wYGTv1EyZQ<=hx;{@VyBys+9-+9Nu&{h_j+ZeTjxqb1T0Gjs$W$4vm+!R!}Z5 zRuR*E(ZXJ*fw;%IZeE@%TppC+8{bymCeExpN~N-V<42!8t(g-JG^%YQy0e9_oL7^X zy8iJnh;uv4R1zP$^VWig{0!Xl{$@=!hkge`IRNNmII1H@VyljMOpF z5$7bsJUUSv{-!D%kb&fZ=D{8_v@#bNpAup5W%BSPj;xO-$KRfj%gPCO1o~@~%c8a( zPtGJAhQqFXCk6_%($3Xo|1&}9QCkmIVBvpu$|203C-D1dZTT7D?>O00AM9Z1n~Kr) z@dWlO9W@`33XJ93DUq>t&*oF#>VSYNI(J2OPwOUGp=PS3qLCHu2}P}jNh-w!OHK-F zkHTAY>hrENS>e-dICgmueGph=;kJGnQSaOIHtFXP@s-aOEo$9b8+AYW^@03e)Vg)L z*+&~Vh=B`1?3jm{#Uw8o-?z0TTJAi7Ol0qZwz|#4=hON+I_0ZlSoSm!i-@3k9NpiS%~XFe{hivvg5G=i zDqcpP^>0+mwfJ#Aj3G`tI@QQS0{?ih1PRekvc21hJWSoDfEca5cvJ089)IJ8(9~yI zeNiDPF#>Wka9VyO^+oTjYF|^e^O;Qgi+OT{%2hsj$d5|QxIOkZ~N5}f80q4CIIqu zerU8*Kt*-<)aY<;nLg$ZP~ZC=YfX8vcZHg>Ys$S1XvI7D3YSr39HTV{ijLSCYSpR8 zDGHu7L;85wykNDf#VLk!uOvyjFy#fha*Cg@5YpyN-wNX!<>6_s)!5+k7Bkmv_Mb&x-PM5}%e)OxMaxg2YH+@KZJ&3KnB9~*2Sf53$Pg$pWYo8; zdIFvdw5~4s*+2wW8eSs!gu(fy2ys_Io@}9ClY-Eoot;X=4Rc^KXc?ZeDca!7oc`!H z1wN7LxN#_S(w%w>?pG^}Ga|#!M?xGT>K(MyA74ZO>3ajl#p!iO2J-MT)>U7;y!)n2 z(!y;Wptwn#fs9fHEOk)8#NV3X*k7r24Gsw^rk%9OK+B#RQ zXr?^^blF1if5P^D^8rly3-v)#_AEhR#8vNm>J@kfc>pVz!vp(+~wf_?IJuy9=vX* z$Gj$A{H1_6aaj=Y&9Bc|SsB(^$e|#bka+m;k4tt9ZTASjHPz1eZyFr8JF7cF7TEy^ zv{$}m`JJ+o#LyYHAk~iUz7DtI-gP;&eaAl#GS*}jC`MNAeW>@cjdP-u^|IZ5FvM`e zEm@qjka`9e!axnm6G(&fG{H8%d`S9yVNxE|Fc*sl8Ei%Uxbxp_`~KDQjOf|MgxShx z(D+ckQ0DGKl%oROyA9P*WXN%I?8w}^5FAds1O=1h;Iiv62@XKxN5%m4LJWZ^2x+F$ zc_IU8mRy+k(178*4AY7&TF^WmeAj+A%u%jrjq8KxqD+c=8B?#qsDld@9K}YcHCxKh2^9Srm|v^kb4-Wi&p8< zzhoOHEv}DW+pqn1&{@O(Smu`ESMx^JqYBW(zsl9kuk8K%hgL_(AkkP8*G1vj)x(!i zb6_XJFNy+*%+K^eq5Y)+TFt7e3TPQxG8b01LMIH6K>f$ahcp;_U}&X?LSq=hP>RfL zbuo#g8EU6>1uaUEjC1;ELt>x6Ou{YgwaLBsyv$A1-)30b(5b>^SbHEX@=zH_dAQZ5 z;O*T`Nth*2-UVElv@Ws5>emQXkdiQ_7XsTQ(@y@PZbWDpVDA7x&&PD3qM97bnzDH?g<xHznACO{AP<1m44w+Tgh95Rmm(O_fem5C1LN-W0r*a$7^VV@tDsE?<=_bQb{Td` zRXmU1meZ2wNFgM8iGbzN1T*%Gjr>l0P_X^`EvB)IDrwG z(j9)bV6I{>A~MbPZjd9tf41lyHBG#SL`x-$BFm&IU3N9$#%kG}viC6QMpVzI=2B?_ z^VU9m#bWF4I+TrnQIX+N4Q_Oa7TUI17cxMps{Xx8xhc^)%XiSK*}uMJcqnlZ;+g^^ zv>EPjsT%+jiabbB#FLhDF%6bnzhbsQK%mvwn=b$Gk(h1@^B^K80IM(Zxvp%s#nVfgbW+Hl_U5P{F_-$2xMRuHkT2Jy~H?peSuyU!MvIi^$wT|+eY#ovoG zG!zDCh!%*$PsV451o@PgdWHs}PZS;L_eFDRSY~^|3b#Rs1l(x@zG4@J#OH#XMuc9C zHV$${S_deaG#K9vbjEaYVkU}IsT7kq#Fb8QpkjZ^!mOEl2-lX%*7e>=VN^Pi4N{EZ zmP=1gPEsFYH}jrZ;BcJEoE2I__4qeb?fDlf1Rpj}N%(B7aUi_kBC+i$IQhnxz9Tzm zB?)U-n)-7mt;893djC{oOBC*aHchirzD?w$e^ru~#E2XkjUZ4Vhz`r5-hFdm#(3j@ zuAE0?z>K`$8Ik@YQthVtZ;dEese@QDA%Gj>ja^qwcf5 zL?k`N|A!2Yy)}dAW}F!;5yp$-I8can-QJj+RDP<7CSeEV;G%WY40mq?pZD*-72jer z_tv4;wS;>%zy!Fab6Czn#+ZE84@xr+*-6{N9{%aiTC}gE1^c)XM2=(S*()~{y)x_9 zBPO$ptHC%U0E>9T`g6}78R~$M_klwnkjJi~88EzsUCrsLKd-G0EU8N>UgjuDqL&`1 ziX#~EBq+w7L|t$`kVU_W3*T~36TbWjAtQb%Wwxw$3!X#RzbR@F{cz>sOjfmQuHB8Q zy&$oLV$)4)Z|HhA$;Y798&~*#eEc4KyuKNeo6eFcV^w5;CVKfH8NmvSN#MOqIW)&F zWN=G=LQHnQ=#?aICiBK{g@@($82*Mlrd*1z=ewyq!gE$xXhTN)NF6PqW*?4FesA}y z+Ay2dtE6oc!sbQ%ycVbfk248rpW9_^x0=aK)jJ4!Mr02qYPiv@;f-HZzzx=yF7vT@ zZ|E!#@L!6YZcNIY*bu1_00jZRxJKAoEVn3CjW6)V`c8$cXZu|Icd0Fx6udgbOh-jr z@#X%Uq{r2E`Qmdf7@yY;eZ-h>^#100U8xdFUs4Q{CrTEk#zF_u$>%c5v{Z(PW%l4? zrFRbzoH56xJj=y282)C8OF`!=Jut1&RgsLj?!+n#RC!R3p81urjus=x<%l${WUuhH z@}83n;9m%f3fkV$j0siwGp)xjEcu;H&geI;E>D*R9u3b2`Ex&rAP}JV^f0IoBkPI< zdqR|95k6n88}Q#~tMfrOPgthH!2c9!7c^5uNP|#Giih9vBQwTwYQmON@M+>ad|;w; z(X{hoq;uMQQm<4owEr={x!;+V0NYCEr$b4MW8BvD<1GAnv<;mk=)85bMK_FL;^ZCJ z>c0%q0IDPaj~{U<=2_H$^JDqVpG_sE?uMe6l7R`ymzykbUqP%wQ$6uV!!@+ump7%_ z^B15HN>KNQcWE}!MLTxr8;611%4TFbdj+d zimmG)9gL0?B^qvd7Uj8WDo_UaHx3An-X4`OxENLYQ<2FJHv7jx&X?3x0J$m6u@T16 zlYBh^sq?IkoTG4}6x}`IDA)x<1{b0|N(E1%GNrq0c@<~qfts2i*-t|7chFnBSoP?l z1JP>a5kkq&xazTZ?q)wGt!N)(C$xigQgvz%WC2H2*ycV$=bTNUczJ6pVvjAbj?{Sp z>z5;6Wa8LWG(25^CLsV#9J};D2g^rYTbC;Jjk&aLH2}Y=pezt}8iN?<>uofMxWsJ- z`!5k&tw{l=rxK)gW2`%PC`hMVZ-ny=22sM6dVc}l4?gBeIId?4>x z4s01AcJt}G1&h0vdJyyg91IUszf_e9JWyjHVkXSkBvRSZ{_>@yJ1|&&SK8|FEC#q< z+>v*Kee19ze?(|R4dl&E3%<-+rM6ILkh&sr=ZV$gzxk90hRn>yY#4^MPY^q1Tn5Qk z{G`O8aP`%g15wo6IQ`gtU`J*DHf@*v{UX#slrSS{9^&n&z8ghTTX5uYl)uM zj$E}UMpgmVM5*1CCqB73|jIF@$=ewqVMSBVxS9VYu{on@_T zOfa$DmB%pUz9=L| z5#q-fGlDAqeAe~Sv3o5$7%dj6J1it~eHm;WmD$iszBCu@AvhM`qp`7eC)A68R~P#_ zQg=RZx1WC=cCl4w#G??4=YjYwBL^I42pdY$oRpF;#l>><&2%Ywn@Vg|fZ-kp(&JMH z^Wn{UvcehuRg2i?6-)mVeFwYwm1gS#7Go_cE#DCV*5~3ja(h>rJ=V-({sEpag+%KG zUM9w(mC~7E-N~1Zj)J6uu6MMhLXJ;o9o9hZFt6vNqe`1AsYjU?!{8&CJ50u)%@3d@ za=;L>rBE$ye-iwc`Ko^je!m~vNp1`t?KTF#I;(Ak+QLy?*}_5$?dn&ubh$VIcNm#n zwMvU!m3~rn_Go@0S@NV=#`YZOXaB{;sfhH;^R>40%XXpBpdGfpNKv%ZieiL76u6GO zHjJk7mq=p}bp@6rNVl*nP$V_`(>S?*% zA>}fzPLMDkP;`OIEW6xbdLFD>9&YD52cFza z0ELepAb-Je$xQ#bxT2k!axIUJScV+ksG>^DkdBoD)8D00F(WcmkHTut8=BD;x84)J z?`>IU`Br@s>tn4@J!l2F4|TrQ)!gYsbds2QZ?gQdmg{~GWL8&Jl))(*M#XTL~mU1anc{a zs`NFO5grg2Xoe`|9F6D(ZfTh08k(x^ z8lVM5=srIK+5<2a5R|teD;cVA zW?%^b2ArmL4!mA3S<=2-J#`lM-HgAb%Wc}%$~TsPxl|Cb99O~T)fg{0mDYN&-N2J0 zxww|Fj8dcD$aXk2lxHrp0U|qcgW>n!Q|1aBnyYVP;4OM);5=IuXav9&a`5uj3Zk8H z-!Z1%zukDU2|geDeXFD-HN8BSv?kh71Lmt#sQ82v4j8K-b^KnzpZU6vOm8axWhReQ zWQPKO;FRiHo3=ETCdm~G$ zIM?GmeH09zvFPIEvP_!4)}$vgVC&$C_0hL>GiZgNPrN-}>&pYYU-4k{t20DDZZB6M zd*eZjs|P;WNTD!$TmCl@bitGnmQYOhjYLg0cap2ez~s}MnbZ4TdzYj3Fwz$jzlhu- zRk(I~^#|Jx?bj|#DVr;iC|GJ(e)@wGL_a%N0tl=d66e{FhK%2Uug}#ZGcJBY%*hUz z^VCDd^?Ze$AGFI_dG0dIfx=g5q~?#Wlb!i-@Y`;GYNK9rQ{`8*7IP9e!e0exz2@4cI9lBo&3)vbws4-!A zSLlAOSEfH8eP_45H~8{*T6(bPRGAONyM6`s)lQ{*JJCa}&jlK(})IJTa zrm!Nq#y5fZ*FOrX%v^wj7wuxL4f@CJY@YsXBls?5svk~)EQ+Tze5E^E>hbZR(+5f? zh~+k8S}b`n*WJkT%ZDC)KwGieYY7{bjqWO|J6FrVYHr;Co4Up?r;C>%nSU+ zbPwvs-eU_>CtD$y8vG?bD}6DSdR}~TaM(|FK29bb`20d=`FJ=!%iWaHA4Yibsy&N* zXn}#kId{PP7WV;N_HXA}@H%i%Gcp`BIfKXg>L^=J(NEh#AH8f7cW(bRXzZ$z%g__R zrElKT^~+&dJKG4K=n;pWpy}NjO2UVX@ULmG=BZ*(SBh90Q^w_dYw99ow(vy~5&%alt|u(Z=Z#3Kp;Cv9(Vt%IG# z`UUMDpJ&oNDC>F8HlyKdpY~^?W#}cUS?L$)M4Shxlkj$brpW!7Dajjg#9Lom7^@FR9ljcaIg99Yq==F1J+-a- zccw!`g^4q7s;hyI4&3J+`cb0<^6}n-OUHp}dq+3*i~Ko3#?U12kPkIgo?j3=bWVc| z10|e7BhqSO85m|Jn`S@4R2`O#eKdTs2LDiK7^4ai7oi9G`TrGuXwsZc>O)L%x64iL z>1hY3ul4@(SD}*c#PUKQwNT+#wJ8NxFdC^mbvC{q@4n%P5qFWx&DAZD?*$yvU9 z3ya#b{QtauDu4>Coz_y^b4>h#MY=W7sq(*n>GxPt!m!10y6Wc|lIFTgHq36%6IPk%+nqU#*3?B%&JAivgi)E0c@JP&b?DVnUxDWpU>;Rd6V7)8yn8E zqG3p~%m|h}=yq+ycY7n=2i|?XRnz2#%g+*J$Q`P25Q3N&3yGvpve0WZLVvfqQjb(3 zDq^GT<0e(bINiyPwA1FGAbZzxPE3@@spPX^*%Bf>)^LLg?K0}NzNhl6JG1QZh^v(E zLR{2w*5@A7$`ELLUW$%er$&E^a0-0;sA`{5m`JikJ`a{=zu3TSPMipepr_J+M``wG zr|BPf*9ucUT;vqZ=|1L_*Q_c)) zwCL*1(TpN(IC(J#flmX>pv~5$n`Og|2HPn4K20~vrs2N_L6-(LZsaFy1XHga+D|iS z;q$Cma%|Nb=J&JxR*Vp}x~HWB_+iZHREB0Y96s94GlK~2Q;NJC^Jib~K@3nkN;gR531+7L0GFA158;F!l%D9?;pRt7ip5-Ei}(1HG4 zt%pw}19-h=;rNbb;LGt`YR9)*{4jWbSm@2sB+no_X?CI|cj?EA%C3%%KCwF(q`a}h z@`V@h(rP&%!bf=Rb|8^2?qY66-E$;JnVr3e^v=AWmD-V_=cSsf#TQm%Z9O; z+c~EohW{^5gJvDWp4?L_H>{tg>}+ERwh3;3pi8)WzHoG$saO&C@m!wjKr8v|gW6^B zDYvlD+|qL0#N622*m&6(`cGC(%!&(Nf@18$wZI|{7Xqr6l=#ZKB9 z3E=bd85uA0qa(ky{9SL?EWfAhSxWE}%F*@Xy4;b`ae)?-4fWp?PV0% z@D?T9o>wU87c?t)aXTf}oF#oU3kXVm#f7TBs`gdCity&(Ax$mj!biKi+bjPinZ@K7 zH{pD-+0(KP*$bc0qDeb`Z0x%5XUEx(QlE;Ru8tqZ!vPVmHzOVoWq-sZoPhv;bXnz$ zP#-;3Gui{t%u@F2oJz@_z%p=*uf+%N~l*omtGBN8TOLWhk=zsjG8rxkKqN^^EITqIjAD~-#C zU93H_eWq}|1Pa@^O}*InQVY8T;NaQ3vClFryEqQtWlQ8Cnz)EG-;)Thvz*e|V`?a_ER`5k$ck^Lzy0DB^Hz2zdd8h42-Ea zr$PlheAsYc2JBTw`b`VM)$cY|ZA?TE0ZTBVSBj6efh)qv)+PFiSEz;YyI&B%%|2TF zguq33k5lHqh@L@5E%oH9X++0AdGb~-W(*ZR@eu-JuXik_rq+uBMJ+$K3MjcijT^Xg>|CTBERzf#dF#I=qvN+?{7bY4>Y$E$6Cm+YaWl8%R%24t;!^( zgxN6?!xmOyo+y$(amIK`sSpZV)Vs+!l75-8;EN&vBNwm%Y{P*c5P%wt`}hDK+H0Q6 zAv3VjOtJsoYt1T+HWK6q4Iv!3Yq5Jfwf}5(*%}kGerwTf!9%QtUBe!*VQ3cjv3|=$ z@CV86jBAW4H~QL|81KpPY?hR_v9@RE26{i#!*7Ud(Bu7Ydvdloo)MJpl|6jU_44fp zyhw{CNXgCxW!w9w>E~gw9H3|HZ&McOBCX2-bj84t(Wsf;A6%B>k_hmwhH=l@4-T&- z$Q@cn5qsjv$!>2__R% zirSLz-}@zWM3reUBZU9$z8$AJM=-vHze1fs;I8Xme+3}pmp2^^FfljjlDwVwwM4nU zKwSg_5OBsgnUq9g!Ji?4p&nXdI^*;Oqm~~bXi5O3idPZ=$iHp@womi5$J+TFOKZgY zCC$fs{1QqC-#D7jZcwd8p$Fr=_#=f}8HJ^X3U=PQ31JiIUypZjCArRO^e)C6hQ~o?n<*Fo2AbmjKwiR#*bt&-yM-S7y8uKKnK^1X!02Dp!wu&-L+_ zLqzb+LKHd@Oyg~1zkJ(q1=_E-vt1U13Ec}(c!sxir%I1ot@+Wnn{>KYx1RPPJa&Yb zqy5=(X_}S^=Dz!UlgN{zrinmfekZ?FHkx!tSrvq#JiInrFZIA&hz14~IGK$pEe$yD zV0C=n+b#=i<9lLf6Hm8jUF~Ox6q$U?J;1SJ+E<^h~X?$F`^Kl|{pD@%j*=1fbK>q!pN z+|xPLo%`+WE)(aGzvMMgw2EP2c}uF~7I);#Q-@yn&SACk22)}9l6Tmb9XWwwX~61= z7~39p9B$Y4_lR5ShS32)2N6O~viHpiY2-jlb1Oc(U_MRn{Ldlr``ht$q&AeqHmYQh z&if+RmYO{gnVd(uTT?)zSy|b2!`B^JQXspi!Uj69`8czzqyVWY1+C{h&>l5S4$K?CfjKhN)5ffEZ=nW>&ZVXQu{)JR#WW@CudAQhJVU2IQki%e7Y0I(K zZS>}ziOPcg?^@?D9eR5MC4!h$ihxa%YLmDZU(W04;?21O?W_YL(17Uq_UDa|C;~Z3 z`24|awuHdO?=s5TrJu@=Uw@}WRs3|`PK#Am+3ku$ksMB!=L$+cBVyW@)tkV6T(C3R zw`zXcBxh2De&kdWa{uq^hhD1|MhbjOu`kkOn(md?q^j?+~3 zJHUoJ=y)mI%P|)V<4;bo%y}L#x4Xb|cW6&(MpyKjI@+sKm&<_6!;0*oJ=mvV{Jkg+ z#PA3lJgyT&(|FQB3_b7MGI~#)uEA(3P#8P{|1WiN$nu zsG5|!Km$jov8Nzp_of4rj4U)o$|}!I4_BF!0*Y7H_D61@mPiLYX5cg+J`DT~bev7_G>g8tX!aWx`{2$1*&i0C=H`xsH@DAM95NSlw{jMH}mMt4Z zc|Ha;y9G>$&(UM1rFBseFfFsHDWwFffV;n?*92-mhM#3-I$`F}$H?3n@xla08Dbi3 zV@OD{trI@V2Qk1BoX%ck$X+h2OIT`9Buh+zAGZ9Tg8Bc`^^NEU5`>?*;h|Q@^So>{ zN|cDt1YhNMZ+~>SLm+S0-Q!1!GZ?@NLS97e-wN&e&}t#-C*x_pGt;be}=}YP>23Tg!&uKWczotHZgEYVtq3;SVO&IQf#ICCC~5drciL8 z^*_}WYl)@|USz7#U7bAq(t6dP#4yTnj`6gv&U9-m8khMJ?sEdLEn*2(dBhCr9J-81 zv^HGrn^l{c%-pUyZvZ}fyfV8Fq@;HgRPS$o-1eB=9TL=sBkL)Wn##lRkfBGB7mJ1SW5vIB-n%R~Mb-1n z)+{t6u;>aqK;ONZ94$H3$xUvkN}MZg#Bv}dH~_rp9QA3y7izn)LHn)MN57=?zANgr zqYMz`5c#2+H0+MF5Ho~J@i}4HHoLDRwFuiR6^%-){5XZGcL3<`{pz|h&lY&`cWZyn zg&EWomY_d9%(-L+qO#mvRtCK?Anv5Bo}&Fc1=}}@IqT5%Cs&9N?ECoA{oIxb7t_IY zDK9Z^5@%i4ZUKH>8=p@UL3r_MZADPF(YsbV zId@_73jR9ybdu()6T7!xea-asnq@IO^AYenU9R=qWW?ff)}!>!(|7DpSJdL%ioa#f3of| zZUj5=YVF9&W;3fado=_nG*EG5vcjtPjC3g(F44_Ng2CU(fV0bHpIKoalX62#dhx^; zy>cUJ-X#?!2ID(8?2Cwah5=*!X;i|zLHl$D5DKkX>6gFhYgZT(B=bcvGbh` zDp1@;z#2;yI!__qUnu8j5Ct5niwA>Ui3^;$Q5g+T5AAjtNq@DClI=v1hse}S2uV<$ z+RWA>d*qZyG-xPS%77Oc(>%NvK>=Js%V+#wHL5+WsA(oC>g=78E3xVxdefbTCeVRL z=V^{-h%}By8LraQz;*kd=x~%z=$Je@b}Jvi^1ItJ$TwjYT(exBHjH>i&BIy((g`5V z(xd|EP?Plu54vwx(~~aMQBndq*GeigY$%&O10H^Fw~<=e?JkDYadm%fjJooZJ++Si zo)I2KTJY?_j7)2S)YXiTIldBCahRu?R}+kl&>BM}yLNtdd)G+({zxI6smuqQ$O;@|jmPz%T+Xs9p_csbadt2(?K%=s3*e%U?h0n6E zFeM~CPHfNU`I=qW!ZZqstI#5}GUoI-DNuTT`0*b~mDB|k#UkX?YOiy`l*qgm$?==9%eoXQu$3$&I;+F|fBDH#_biM><3$qBx!^-0(6aoDY$D4LOEA`*n z)G5 z*#vK*aWIIQC+{pycQQwkQFbnxo?TxR7h)u){y=s(ZG`1irh!eNNV?~gBST=o-9{m?>HOab~)RDgh2cQZ}#_=Gu^~EfK&b* zu1n(i1uJlrZ`x}BQ!j3YHJ*RV3YScX$8~R!YFO8^-TO|Xt{VU93K5w>j*=zo_DPFd zib(D$b#T`lQT=Aj^^X*g`mRi(*7&~zTbKIZV|dE-W!X6fP>sIrSc=>a`UJ4RjC>?YO>poSpn18gh_3>ZswP*^*I+( zVb^+#W!}CJs0+fO=*)}wc8^hB<@MO?K`sudDJKE{aOQbUXJ*;DL6j`nXigR(2Ecqr zd=Fx-CBW3r05;s1l$buks=b_T^_pHH`O|fT+#^VF&}R4*A2!jZB@H7)0ccL&R*?zi z@bMk0t~8D8Z(r`8{1iUJL*3$9&I-KwM5q#m!vOPjP0WRs`=D|HxaA+reW4tCa}Q+2 z2(Z{g=l;HFlgR$r9I5(u`+$@Xlqha3Tiz%xfa2RFk2ILUC84?2*}17l1FEqgd6?A( zKs|pinDrapuzn`IRS&Y4@#dE&v**vG(65b#BX$$<>Xq?$+zuw%Yhr$YDFnS0Axqzn z&_7@<5Emg;{!@=`fZJnFZApk+T3m8@W7=NWA<=cS|IzyLdS&Y2R(D_ydVL6DBR8u3 zTedWJ`*R}o9vwb}w%^FIevn`jODsbE`A-4*B0eZ1S`aN;K#Zqlxy1nqZ%8mh+1MQ_ZTZhBSytKATS6DX9EiDkzl6smnaXBum=39WQ9lgyB^YL^hw{^{sg#jRfRlN|oR!V%PXyV0puy^qlThbwC~-xXx^CnW8BH%T$6 z5@+ow*`jXoBw*34-5D}Tc&~BF7idDOpefKraE~5beHOCsPq}t(4j&9{?#=zQk8Dif z(M_T7zF{rrR^TLS6jyI&Sv}qEBT%UP^w(pE{_I^VPwFhbrW-|S(y?#j%IfQ#WTJ&8 z;AM_`&F0}b;&f)IDJ51l_q<|7i#s}gf!;in^0AiLuRi-;ZAS1&Hy#q1^=^B(}S4zaItCZ?!Xy#y&5rupPG=t90gtF-dl_7(* z>{imNqQzwY9fFKN`FiPDJuYtZCMS^!)q;gxv3VJ1K1pbdTR#~A)X@s#Q5xlFWF?F$ zz;Zu@vZc*(nB}~lgm3hi|9Jf$O7B_$cQp^N|A-S79EqF-U@taYNf1fuY!8_BJ?W93 zCoHv|uBm1>>QYK##$gT?!qzG@g^Q-B4}FDED|i;m0u#p)2Ack0Gotc7)I>m{=Avw< z56P=D_ro)LGC8rz1)+%i&!t&bgP9VicTW>JI68TMmr*S(Dn???IF`cNb2@r0t~0=y+5GFtJ8sB{u1?qUTf=Gv>J07 z4IoUDxt_*uV%DY>uI>uJC?JMR0KX=5^RH{(Ca30gMD%H5>yh)tLCKYuzl z`cRK}oiPHCC=!)oCOaNyOHTG@3S?mI6hQj|WyE|+6 zQ!KIWiswS5TlcL5mx=fh0kO+P9sPE`+ZiaY0 zYmF0Gw-Lo!OXH3TR%wfPK`+U$~8nyJy}`3U7k8jmhKA zog1w}K>y8fs`oqj=#Bgz0GB{$zX?zNoeI^EhV^kt1sTCASn0sH%N_0SJ6Ye_1)R6; zMc(Jrgj>I@T)ch9b=NmGHi5gjxs}c3@;$zVGx3$e<;yp&k7dGvQ7{>tp3+r0_VWXAB<-V-7=rnX zNUAyi$Kj|aiJn-DMlx3lXYOv^bMHSWmP(~^MNleL%GGSH=VHI2to&9`2K{|~zTUv_ zXmFCEdor5Dyd8q$Z_at$xm3#L9?;knI6`wri0=FY+Y{Zvvb(_zrcU|dQgn&rn2DO= zhvrkUMV6SInT6;kJWErX?8FHdW!{L%YHT!f{ql{WZFgg%SlQXx+1;zv>a|9tQZ85B zO?UfF{sL?dp@1jbA$!1U%7bXqG}(-10*dJ=@DI_GCL%hSPNW^}Z%A|pDZp|XqT6cZ zl7Hav5?vyZ12sz8{XS@pDff)nQzy|aZk;^Z7NRq5nJi&NCP#fYFDpG8?na}uv(s$0 z_V?>;*p9GX-7HcrzkVm*v%1g^*+a^W!Lf`FvIo(XhwXY4z-q=A{R{JkVUXzD6J1p5 z9{&>^pF_*eNZEQYFnIou5?vyZK=bkVm@hb~>nd8^24jY3hl^g=$;)c^x+e6T;p+5c zc-UWvUpP2$cUuoZtJCV%>-Ac#R3VDR;{8o`YYU-$VbBZh{b$DdCc+fkX#$6dJ6EHL z#W~THw~d5hL1k{Od7h^PHdXGTe5d2(v^9t>zGkTU;ira_NF<+pIS}{vUYb%>vAV`o zf&nUhh2(e+Znt)J+U?qtr)6aF#UgCq zMk24!1n#%Ojj_HAb?JiBgsxf|>h%QEG{!_zj;H^Se4enGhOYoi6SA-2Yqo83l^(u9 zR_KQ!k)FpdP7IGmRD0PF=tPwAXUD-Z%BjxgWJZw+Af$4ADi#j(6;>{|PoF)9=g_?M z;zhmQ?Y8g%=yW&edJraTIOl90isL(uxXTiLH zTLc5%Y%M)MBf59`WMJYunYl|Ohw$Z(-}Ll;KRTUMktrM8Frorl9CzkUEqYRjo?_^r zPfZSw6_lQ>XU`hVb{BN^J1@F#yDh=PX0y|2Hrsoo`o}tQosQ2 z%5|*LVZs-rG#+I;5D`5E=+x5!g9*(|2dw{PFR`0(LF{l$xRtF@1G zzKe9eL36i86`{Rc{Fu$=hE~rgzq5DdPiKG2UA z{no7;2s&iSTdrNM8@%#pcw_=ePry7|B9YvhoL^Z1)?+aMeQ_}!i_b>mv8Cl`bkf$^ zI26}B!dg0neemt~ptc69>Ea?8C6sb>RA4%p3ye4Yl>Bzx%KG z-655c%$@BI96lneifovY^{9-ODL6#vPytb&P}c?ubu7tm9+734$+A&)IQxpfjHHQ9 zLGKXBkFE2`s_cV5OWLm_m5~g`OKhiP{=4?F9nuZ+koXOnL3$>I?CKkcWFF@dqwtWkd@+&J-@Na5r zcnD~Ru>IBQ!PiHjknUZey{7hBZBtuIR~PylH$XTe0RLbo$k%#XyZifY-fR!|wl!U= zsXP3Cvfr8b%g!J0ei^@05cVOysL*@qI37l#ZW2jmJEb29h&z_Q7-3h*mKaE}KcX>` zSJ>?u6NpX_fc}R=s*D|xO2qC{CL?QUsFDM8uhYP4rlI3NqV9H%Hcj(F_vt1#9<=Od znZ_T`cKdc8tWm+cJOG2OZEdxe-yMDTgNgCU*@@LfNXn%qCsXOIjGiTDP+;m& z@pwFjmOi}|jmDvjZhU-tXas9(z>y!l4~{%U?Po7ugUXstNYV8~V1m7)57X`)SX0;9 z7mk2&$Gbq*RGzCS&wOVjU7;_UIwni7>dkytn!!})fB=&TKV7%YJo8zP=~bbqPe_uc zg=EA`+_AQ#BY<{DBPS!IFy?c-M`K=5x6|{y=mefj*ZU2omq_2Ibe;cFLj^$3e%H(t zfKJHiOp)9`hpsaUfDS`O-(kQ*Oxr^%s*asGbFR7`Z-3s_+aKwHDK_Buj&F!Cy-5GOnopJ%dh?JkYbdgRbe! z2;`3zMLr17RUbWqKT9e?Vzw2%5a=|JgrF0VYQPP29Gts?E-B0-iF$l5tthupKYgmE z=~`oJKWzF2iTCU6Rt2gBIYiny`V}SeG+KY#;e5R)?99B1NFi}()R9l)UKdgqz`;E67KB8WL~%jGIFR0EKR>~ zYVgV{Plm^*w-EHHDX`^R$&EDVr>PCnvs5~r0wL?M#bwk=Gzvlc+VUF2?vsy85$FH`|00aE)z^ja{8>osG3M=dT{CpazS*b@Jm;epR8y)A#6Coz6Qw z<{Wo-&^@wz7(kbaqpl8+9(sU72+Ys)C2><5I>o+bZ00z_wT8a@QI-WlhaR_*Gwb`s-B{JMS{{ljFX3 z&}F7DrkZqx5rZDg=iGmQpp)kgl9dv*`;eFU^APrnK(}X0ct|w|=w{v&n8svR(9v*< zK=<;TYldtk-;Zcc(A`^6QBig7bS1Km%hGXGnzfuvY|&Be-M#nfepU^p_8zQ?X=uRA zJtW3kTdpA2|M`zjU`GDR;3y>N-l>L@R)YZlstR3{(3bM-n8YVDS?<}xWn}^C7nudPZ-jY; zj=MYP&>A3hZYd z5$Md4>~~sz0^Q3{>S5mGxPgwO2X=z)=kvt|Q)Q3W>r-%Ik7-4=0+y`ed|UnHhOTe~ zI0qrx4buc|Mg3k&NRP+Wo@+$En)EZyTfgb_ugrK3?d&t0H_LL3R~bmrvi$-+J@35pj(c&SL+_c?k0Q_wu5W@mQ+!^p zM^>3yw&!GAR&-)|iYc<3tGs|C7ORpdmn&txBcCdXK$l^JcvMy6L53gvL&=nxkG~SQ zfexgrfqc+;CpDnvl5&z)WlSr&6%|L1o_W3boqL@KI(qJ6i{1^}!}RrcU49RW+kf%P zpIp5UhW1@p?RpJD=kEU2whO14n;$(GpNPiND{;UWF&BUiHr)n&3qXfIHeYUnu*_yA z5nHykxQH3~=*0Zg+Ug`!(v5ud`LE7YRVaU&&#ib;!4t^hN`2$uQf_{)&W>dJCCEGP z8aeJogH9a>$O}N{LnnihavH$8H_#<5BZWJ$mXQje>r9uZxy7m^SL2aYvbj* z`_(^u{`}XkR9=Cpu+w$*HBC)057yOmvhIH6$nexe{4<<81CY0bprDwl;0O z1SOJL&_S@i9hKgioqqiI@$>h8P+rcwg+M2ZsvA4zM{RP~njeo*|LrUM%Q- z7Ak1bLv)@XF%R+^`Jfv+k@m7(PmJ;hDJ?{2E=DDhT~JtvjE5L>P=GF{K2-oZj4Ua4 z=#V?;NV?<%oe+hGL8k*C?WYz0`n_|bzp+8z-F3(H;^3nyttk7~ zGu4+Hk@EI-I~gY^m}!R_C_-6U3zWPLHXk2(0_E*59-aW!o3GT}Yl8Xila=>hePV-N zz@nq+f|Qq_%p+Ae zNO(C=$-ZVkNsfE5p!;N%dAS%u06Luoyoy2-qmo7n6a~7(yj1j4oJx{7uMdU`IpqS- z32*Wy9iS85bWLyr9Td#Fw9C5FU-dGQyq{M5>36H^n_9vhIK_q)GXoT(bD(2Ud^;4k zH(3kT#)n6yC&yvg>G4W1>+hYveDP#`Gnn*=+4KV1b2Qf=XV!fqMaqJ{zO=LgprbOL zJZwIPO2rmt5)i8|FRzV%{I#PM%v*pqzEvtUHkWy%%!tQMh!Sj+B_qeZNYGW;(CG3W z5`Ydp<&2^jCyhWC%N=y8@G#(K(JG0$OILWS;olW>jW_oq(0M<+5a75%NGr+%Ay-`W+yD0C!%6{hq+4cQ(O-wTC0aLld*};}3>D zdgax3V0Y)bH%?SvdShsO0``xCIv$lkCEZ8q--#zoz=i`%~zMQc^ z-vBvXcfLx_0lMwXXF*TGnzsZ%Zh<}rlk96_zkcheUI28!n?^h3Flc_n$I3WtGG&RB z{t`RxMT0JxI+KO9?q+_pNByiF6zI%T3efi}8Z&4nD03>SU%W+UT&$(>AK@c*zdPt| ztthWLQ+J^=449*K1>HvddPgJzyT#qB92 zvle}OS0U)>9OeLe8kDjHwmhfUTx@YMHaqt553Bxv0ninlkJVM*j{1Jd$D8qhlXMRY z@{ardK@TZR7q3(t zODoEcT|L~BUQeO--B_ujt$ttaEFt1niUS61fd$HvE> z{ODJw&s?f}GQGMv;|4m2Ku00y_0S|<@?WjL{^@(FV>=rqcL z!mK-S2Bx{NMHi4x$vASp-9UF~MFkl2769GC{4U*Cafd~>K)-b>a;LScrS{(7;E9p( z`PtP^K8eCkQPX4NV?&>xs66pve(KYW{ezC$PW|+!Kix=fpk5|mvhC^m>XYZc_->$F zoOS0H>?pcb5*p&Cx@hMK#j*56T*$l+&>5p6hge7qIzJIu2y}lL$t=!wH|TvusU%9e z`IxOB&5?*fm-q-;5`fOn&3OufE}KjdfXb}>3AcewAAIN0^!V%?u1ASZOitpm@&_Ni7+Z@*7yf6UZ~W8- z9W8po0zE!G{@~;H-#VHhohyMgok_O z(P+>p{zIDAD--C&wxZ(9`HP+1J-4m{=yw40JlFkA{+DIZZ^0^-w$9p%?+#WzoSvPU zTmNKk3l^h9aaZT**=Tfe5kZI5=f#4aon!~XL1@webRc~(u@IdcdGO&^e^6CkE~<>9 z;qBKla;!kon5<8%%stt_hqpn$-hrfF#szDWv$M-kATu!m3sXR|QHakMW3*A9?Qw1$vMvSW+KS^ zu%M&lK0`J~G0379+loJ}x^lLmH_}6(g9!ATfOQ0Y;Fd*tPq?MA;ogVA?YxrbMjMWbk}sM#yq&-N0%A%@o|+4 z=v0($u#ju5yDF1O7l1C|>of(xRJrV6LC;nLNP3YIY$Z_wMI=Sf8zLZI$bV?JM42gr zJVCy`L2?D1cA23b&tKXc^66ZRUTiD==2GS5w$~zeyP+HgA$I}2|L(V8XPN5*cfv5q zc4~0&mB){#=av^1UM6N17m0KPJ+Xh#X|g?mq{ngZ*tzKJ$m5@X^*>+}DaOf*A(G1k zDY7GJUY{(R68SRjuDhahfwv3jToTq0cC2#`0guesfr3umY!(@(mqg_jevKqi36`*8 zm6lJ?9Jfdza4KCv2l?_gKO<8kkSrJ5ivR!hQ%!9hk%4f}UO>OwapyMP8m+q}jG!iT)o7t1=>S2e>Ih2V zl;kew1`|3Xg(xyC0J@~Wb)XRFm{E5FopzaVf=;o!3+V2xsQB{fvkk2s5#0INO2Q4G zcDLvqeSKikBa!Y%xUH?>-rJ+cM*#HY1&GjNiwQ``}C##@{I@yM@#S5@I;(O`7#C8wz& zgS)84pnf%pR=J=>x0S*lNnE@J79Q8PtfZegJ`3frF^JJ&XTmZ zRkZ-<8r5#NfKI#2n;bkh}d_g0jjsjKaR+aR|C=mWTF&C;%*XDmM6y0|CO-ql(A zv(az87@Ple3Y_)Dr^)2gr%BlSZ4MWuY!n84E(QUoP-_j!93)hBd?A^H`7?$?f}v{jG*bj_3*ZOZNnI+bQSj~CRLUZ&+8S(l4z zMS0b^Q#R<~4CuED2|&+X3Gg<@us9v|d0tzMPFVL``)mUQtj2)M!k~kmy@Z8qz&U6J zYPlRf(?glB(3_mEauODH*8qX;dOKB= z6|O_P9&Ty5c7C+-=Pw?A3dy(a?Ic>@RC;v_FbB?EKu;t=3DnHu=87fjwv11$_3^i`2!6Z>mE@N> zIov*(WE_(7i^0lfRf~sNg6cRgH(ZJBe-*_ zjN}B`tFw^9F(P?(ezPI|lzAOX(!2n(Ef_&wO`;B_W{6H9ewL)+rm{Y*#O@B2RB`P5 zh0gAQj((84>u%X{OX$M)!L5v2+rXq(e>k+Z4v=mpo3}#}U%f~q+xDC~{v5(Q|AFXGxfRjVF*$5l&56cqFZJ~+UPvi}=uk4@ z95ZDQ6w~1!0$D#d>i*`;!_0Jntt&%{rrRb-h?@Wup_oIazxLXPiSDzA5o-rJIBYq& ze*K33+6mkm+09I{>cLpt{PAUqcmw=@`s2M`_G*U{#*?Z~0g?)mYM6T;YQultO- z93>$$T1a$&($3EK{DJL5)w1n`j!h{J-4o_6h+0%p_RT0QLSZZq4GxdIuyk@g#^e4Q zWSdTZvAWB)NTldncOPm0(DO|6)&}DSQqgmT-rla`JNNc&*X+O}B=*JD7D}Uq=yQDe zIXWsk*9>!P_rc9$O4YPI3V4!<{^ne^T@|87LUco~sO70guRd7!Uokx^Bk^l78Mlg& zNYU5t#ZX+Xc)D1;BSgOi(OV!BUCgA5eQZZ|jqV-PZO-G@>nvuB#qnG#i5{h-%j4^Y zHMDPYU-!OBMYFwt1DYlJTuLUo&v{_k%c@!`zw`9uf8KOVhQaV3eQTBP{FF#GtjBU0 zlyN?NdIh56(7jN&+KYbWoBz^}EapUXT!}#veV)wp&n=i)sHM~$r!sh;uXF63vTA$a z1JU1%qK`#qgg6H0%U+1t^zAVHKdU06L`SPPZRFHTu$!9@>Px@p>!*J@P+CCi+6kCq+*y#@1?%B#+qf%p8t#lp3L(`SyL@#3IKn$4LeBL?%uA-ND(JP@ncqpZk# zqXAuKz(a9;=zo%0&9F6P;=r*3?^J_j${ozRxt3+cahm%0osVz{Fbt*i>b+J>mq?`O zyN|S8$U*cXM1TGnL{ED)4T#n*x38T)he+?-p?l5g=_aKXtJRoLchX-FenRvGA^HMD zpA!i`*O+Yx;!l9DFi%(1cXs#nPw2Yka5m#o%Q92ZeF1JGUr@&heJwVeYB?(x`ISf{ zlJ9NZd!()B&YgTFdM0^`qTdvv=TUj;+QoAnhY$Vbz>v)wO+wv-)d*pt+{_Hb#BC;e z6v6EzgL7PyIQf{E!D?MCmqyOso1ah>n-eAi-Q)>y-6+CE6odtJAkOOT>f#IcWu!|a zUq6k$_vo2oKEEQ-aqNEQX7S?1Yd6oep=I*febtZxaUKoP8LKnlW(_OZeRv*6NyBZp zmVk3h49)XTK!Ii1x>|aAY|~V!YC6!{q7c0Sni;O}Tx@is3{Wpn2cMp7A<`uhDSCVH zd|`#6qm(3$`!D9uV0Gt-z8h8DHbtj2I4B^{U8w2%5M6X!ahr`M1ug`} zP{^NaJAAiu?|fM`EjC%7WNw4g(6$YlZ8}XwXKp5|3lf(x-%lSUu0Xse4)zUGS6(>z zpj6QV>f#kwXr75qaUq0Q+%s$s*kRz1pt`YzNS8=tqPOK%BsxGR<>H0nokF3|({=pl z?#}HcRk7IgBugkq=-Sng)5&S*8)G<4xDCM}Ekc=zA^B*s(Qw^HN+vo^y&X#*+-AoRG0P!^>32>ckL>qDFS#&pZ0%TAz$YPni1S4#6EFWfkA zVClhS{5`n5w6yg8)9quArluAjEw!*bP9l+_=L!(LH-GgG68+gl2Uzpoa}JAUM*AxY znTF_^M>K8h;MsQ+;~({h(TpqHCWc{%mFj_MnB#$3sZ>-=Q&rq(<#HJhDw<}R%ciP~ z|C7D5fN>k?;`mBMF;iLmiM16o#Is2Q-`$njDsz{P@1im%3P^61J=li!ie9+IgEEtQ z9MaV>1e~!QSIqauDRadzzc5(*)Biog8{hInX+BMUxR=OevfWO>@Al61ATP@@UY6rL z2(b)eBawVpmn2ZnDU^3D-5-liI`#Jy{qUYWJ3Cri*DXHw(Va8Bw&-Wa&L;mqH(H4I z|9;`p1?a#>MuvuZfq^)mO2rd}2ANFZITDG)IruqsV)^#My?cp7A=~8)gVo4`T4u?y zq9iFIpw2?Fq87)o90C}o>FrP`G|{&b(Sy6}k?p~EZ`H|YXgkq&!_qD7t*b(CS$e7= z`n79(eDCr(=TiKyKVQ1>3t4_)pqJ#A3v8m z9-iBM?q6R%8etGKLn!4`MJ!jUnpUgTurA6D)Fc^ZT~yRuNuK(au~HNY72Qitfejpn zQ(%wmK5*cD6CIXsI(!(0#}d&ux2}SDuyKC){N>@Xfq_(f;c{0pHFW7`DBDM14_h*k zInm{FK@rY+y-Z8I?@$Gb`|=_0YnG+C07KjZi35^e~7{>hB}F?L-gm ze=pH(0u4IXubX%DP9qZV@D1p(u$& zaBP6C8pbNv>iAs2HEex~R%1S2qkxw?J^+v`kI6j5_2#gg#K5!8DV! zuNpdRMn$Jk-bM>dm}!UM5glYWBw0R@n7-eBes1uq{pKan_t^xu0)Z`UohzVrzjJnM z=>E|CzP^U&=Mtx!ZjUEYQYv+DylR*ruZ63IR*oU$kVW{e3jUK{G~G0enq^>IEh5#j zO!ASm;q{8BO2wEUPlhHQU>Ss1PL37ropjP<+N4t`Z`zmV%7 zc<`0$E;d9brU!#t18o~uE&KN1iTK#?pFjP1g-`EY?z|^v^KK6pQ|=&Y8xH&ZaF6;qkA2vU zn>Fqo_PSFjbi~ZMZCiHkgwYeNWdAw%{Mtl^*XMYe=)tXU|D<)*((jJ`KYgtq#>Nc~ z#C@KqRN|PhOR3<8H4T#j+^AQ`F)?tL^Lh}#FkYLv3}V=bT$EI?Qnheywq#Vxq9m|j zxR>R^QQ?8h*>&PhCR0ej?OY+#?Q^;wM8lC8O;mKsf9#zJP*dBQ@8A4h-h179SJ4A| zQ}-r4?R2HbU8$spkRF`{6$d~(sDOe5Wh`oT_EaGkk4Tr=%^f6!Fouu>Nr(`RT$#iP z5KrON(lhitch@}keMwb>Rvna;D zQ%sAOT{LLDfDTRfRC(*5-Xba_ zoo2U-iiYR|^yq&a5`An_w}{=KtO{96iyZeoqE~o{qSYd?fkfRMKiWi5hwhWC+;|XI z-pg*$0j)|V4Jtw7c%r1p7W?Wasdfu`S)W=fkTP?mu&RcYc8^;080h?jeAT{xZ;hH_ zh+}$RQBiT$QB{1T{8Xs_x*Ov<%;IDV^10s z-Ov&WdjrmXkIOmM{;u`?i|=mRWZuz?sEsBf|&?^GrCrL3guNb5E=~+!tpO92hGz$zf?T(E} z#fijZ7C9#aryC>x8r;%pbvyq!i9W!#-1P>FB5jerK}iz-rJC0PedW7WtQw#ba*~p+ zCIcN^m$?q;7&6it18!Et6kCz8=i1RCT*INFzOfbty@_ESU1p0b`O2tXtH%09rkaYV zs;kZ+8n@5o1&EKPynU^E|N3tccKh-tFK)vJf3dTB$>nr8ogQD*Jo^WN*oZncF&PfC zD=-cCec0UAA8Z3C z(mmYVQVP;$+a#?AIt?UE(1}P{pA2*i&dd*juBL0bXgEZwtvIoJf5ACNb#--Z&DAT& zX$9l4Wi}ExZ_-jleFc*P|8T3;ajp;rrN+F&4D^8Ob=Eh3^S57rb8GwF=J)rO-a7jO z0e|1*z(6qaB*;oZKU;)`gP~~cn8hbU4;CXqSa)B)=WX-$6EAjkk9p`4mxC2gb#vl zRU}d4K$`(NHJ4hkrBpy|>x&MG&a1GA*C`!z*`V$ZC{T7=Ix<%m*EnBvr-GB`7)}|$R&CHt z37i|~5UI8zD`$T}aZT;zt5@0fma4j{y33a@--znd=>IGqrUo8*Erg(5Sp|wmUAp+a z&uXjd&z`Nu2?FkWJ&HuuR3T+%cYDXcqlb}+P=EXTy;--;XFv6NeSY7he`qK&gIZA} zRx@stX&R&tvs!bwe)4oM=ySH_HMj13)123GxV^(Y5gM8sfueP4q8c8?935^$@W(#) z>-YK$IxQOFQ5H3(H1urjaUJix3yOgp_m2l%P)rJD{d9)W2Hl>>xnUDQC(j)c(zmGH zfOLu>0bzp{=&{uj9#Sz5bZf#An5YOzL018^7U(*jb1_qh2H$NWJ4DK@*pZiiw8YVL zy#L-MZv-qKLa66<>o7BF|PChvR;K9E`_Uhvon;P@kR;4Fo!}k!$n( z?dul@bFMEr!|(z^C>+UyK8$JoXfPP|wr8jP0-d_z?U<_2eL-1$)SQ;4r+yI>J@<_$ zI?i!V1$1-*O#{q|UaMnO-m6f}LNxepG^-yEdZri^t>Wp$n9K@4AXrPbsNH~MA^r?( zP;6SDD^Ups-ewZ$>=u)3=1opA&>89awV)gLcyXp83pSn3XkrU{ikNyUvMNs&oV#%a zq+`2jSyfrxXUirXLOu)w9WXaFUA%Y|FKL9Yey+HRHa#}7w>!HJ>Cfk5Z( zgT2U}eZF+PGVfZOb86Dh4#7b8N3~oA9zMQn>5bK}#%ixT!g;gGYU5=EN*9;=1^3f= z=bdxhQv@BoXQh7B0^P#(ja^bqMx74J`ih*E{;{C1i2L9b{koD4JF?PVg!i_TswkGL zu6RdAS=9nvzzDI)vdDvs|M|wjY9P)}X=*9}?bvGJ6z40j)&q@bjzsJIT!d<}c1v8w3U z-BZ0Cz91|2IOKI@AAHl^;X#!+nBwu-SK|nl$MlS8b~>tM+(#cjo*o_^2>Sx=>jzuf zJt+3-bY`X_XZN1#~q*hYXUe zk}sJ6dhCae_omkaouYOLpmP&4b{-&EB{B6@WbD|Jf2_C)*J8L7Bfar6GUx!Et)RiL zzX-z(RTUQu(3J1JhIeFclKzM30&_CFF{*#>E4yV%#zLziV z;)gk(faa!L&tKo729iaUENj+BRw2_x9VFF8iAP2}LL$dKRnQHJd0mTcrq8hx^N_(3 z4?4N%VtRbHYkx44!A@Zz`s6L8BC-qVikXauHt3L^PNzm$19Xfmb@kBZ zDBZQkI{@kiT9IL|JYG=ZsHtF}R{(TG;Ya{IS^<*9)vHQN&z0Q#VR^$RpER7hd*{yG z9~{3~P;#d12F!g$y`%W(5AI~+a!R3TnDnq~@LJw=teJ;!VV1>(@!{FWU_Gi=xI}t9 z=y2Q-g~ydzoP*a6=5~4m6DVN8P;U98vfG7}SO371Ii_X5ojLAf@80}Zq^>49D0W3A zFD8)`+rf0z1 zj;4cJpp)&S9$yQ(NSI1Oa?ly+>87|I*Kw0i<7TuX?RQS)7ZjD&eR}co#rk@HzQQPm|WZhrr6&#Byfhr7Fa_vPoD>iNO((FJ z^c$T(H9*IwkXNzZ!l){e^eKU^#(oDAPI+5ORVwH8VYrY}t^qpXP2S{M(1ADk)5$@H z>Te93QHaJ74-nUZe?k$j)9KoTTLxo^Xj@{RBz9}+f#nM z3sdhte=t1X*>(L?PyVr@y0YqO_yWZj*i}{g!$^VooPj==+i-n8ddbCw*;z*VtJzl& zI8Ow96xU0CJQwr_y32FAVZS3NGC6`;bFSx0xeYnp?H-nIGl}dOxb4GH=iT%4#dk7j z(DSXLcC9>U;36ZoSU{9uqbM0U?kR#U3l@=1?`93q(NpoWD20&iJLj?JKFXHP{eG{zqYb2I zw>kTqh|eK29(l4;Q`phc_U3j5eHE%K&}iq}7Yk)IkcVWVwGqm5*LK`f&Wuyjuv)GK zI*kg+7#l9SWmz^0bfX+QlW$R*x&Q_@p*Bf048BNIzOwT2=wieN|C|-iZ7+fPSN~wD8FHdk*az>~K2~VH+Bq9zvpP$J^}k@&gx& z&Nv(l^aBiZ-@x!gB(7n5nfvunSmuDMh zOXd^(9_LbSUf%ia*29Opd!M#-4mzuTlg8fK?C#@ zT0yh*jI6wC`%8*h&>d=OWMY%_rivKoHFd?Ozu&XF*BR);mXx81(cr|SKj3jcZ@<>? zgJXpyWw7XH3U2lQ^nn4E%Z8JcicA) zy4j>i+PjtP+^Dc@I_P9!wkGHs5&@0DLsMM}=xTwUY>qB`{o&xmG@FCt;w!#@$2r)xyXW}P!a4?e z;mrn?8s~p9GV*XbH0AU9CWn|o$0Qw~Kc1b91D(_2985aS4wJzFoEav=0bc;5KOFgd zXx`uFb}rr9^(MEaa?kC9dm)sj!M|-8lG`F%apVw+9zRG(%8bLOaXIk(D_kj*NnOoXE4p{TXYT5DHuo2 zcO2-cwt_%sq#r{9Ohwb>ixo&=M}m7 zki>p}ML?ZQ`s_1aUwrXuCc4!eFS`GL{f|8_<4g$JqqNLa0P(ke55!7H4UV}MXTMA6m->0qzkfEFb)*PZ!c0H9X-7n z>*(lb)}bkDODeaUe^~}vu+jjX=6mFsCxC8{P28kvebA}O3=cZ<(P+@58~%%^(+LE6 zs;!_vFeCjqR@qAHV4bmY-c$ymIBZ3AQ`NbWANJ(BJG=p(+v)7xcP%&jaND38MPhuu zaJcQzo%|An_32xH!<2D;8M$Ae1@@)1F^K% zP$(P*>(Ep`Bfi7KDpc$|e_{s;Ptfo-h|7xGyCW)sxa_eD^u))2ZkH8EwkH9dxehks zSPOKc!~to>(>mRUK&M|OCD83G8(L<;z|^!QGQQd=C>o&ajp%0Tu{EHpRtf{LbAJ-h zsmcr;g*I*lfu_C%1{+(g59x|7fR@k-YcK0^P2dM6}l|OIxI;DoILOc7~r)v(kKY zpUuVMK~ERb6~(0IC)i0qrvesql-2i;%}LKBi@uRoP)ry`efP=z1%+qIni%NyAl*@W z@iL0X95}VxIoLPl4GdmCSovb-i@n?T9?Yw}*2c2dyzcy-@1HI%EiF9IaA+wo0mNhT zZg37*nP#G^=Py49It$8!Zh$_+IA`ygnPsFS!+j2}cfqRP zJHP!`73=!pR8g_94L_2gC`Nu>l>~H3N{018(dB*PD7EMVONEth-s51(JRV800x$AC{xQTZX(77P2MYCh$ zYY5l`McGu)shh1^^Yj!&=@tf&q*4l&X2vRQpP)5vEt%mDCIuZbCKNx5jo!>8q2QES z!4DH=)SL6+zn9ilU%ZUGI8=TA%u#Z)r?TyNUpVZ2daCl?*xUD?w7eS|8|!WPB&XHo z^!EFr@wmc*<9Bl1eE@xKnR9sL034J@L8m$TT8sW{1@zhR83_N+LUT{pk>DY)e+v&s zux$4jw7+~g{@_7yZUQd6`}vy_zp*;nNqUO9@$B$rDw2YZhv}@H40MVJikhw(Z8Rip z3h1=1NXu*~XiHpy2}vQDsg%PypsVR1Xg$!a3hYpF(5cD{^*B|C)doHFRxksnC(<)c z-{u~ye997HJF%F4 z?AXyC;)1IE0Yu@LO=sRa!s)MO0Ovp$zuDiIv^b~09Ag`dFW-#vzJQYka}egBQ7Tmt ziHwZAdWn@czx#P>){fQhlq3@Yq6X;r+LMCLm(!Gu$v~%#L)yU?IQJm^TLfL*0)1m5 zUrevq#1%>D-18csQ;}BcAx+L@Lvqlm%1mO9TM*CD27M!~fJ}U7IpaIG4q=x|F(T=; zS1wkUmICzLb{Cv-_xrr;=j|IwJzvjdl_(;cUXBSx-oT*t+GR-(8L5|Jv59e7BAt!`@PR!@7cEMyof8(DRx>} zAa?kY$f;O#Jf0MESbbtu19X;;XaCkf=MS*ux^JX}6rHQdMIu|N3A&ml3 zJ|qX7s>~$zIBrN*ZO}K+3P=^=>qnoswfn%a!lF_KR?UjpZk1~QJv`O9JMZc9cg^S9 z<|kQoX18ZwUhbebFgZVXt>MtVedVVPyHW0y2VEO(EzmXeY%XTezhJ)q47(F0f0Qk< zzkK-wq6s!1`t>mde*R@;`!bJ_y)KA*EYr{UVZN|cfc zJ&J4|=nKoB&$0mB+#Cz!_vE!E1Rh(2*VNP0$ck?4mzL4 zU`lS$DgTt+(f}PaS`;PIYKR{$nVSMST5qSHAFnN?E5fp3(`^DnSeAtDR3w2{y250# zs?iWs#`gp^8aLG zOEWX;7YYlH0(2KZe>8t>U#IK!p7%?A*km>ooSN@{+U=giTp1PK@(#UkIMmiR^!fPa zfzDj@iP7oB*@swsn_bb&+{`f6+F1Ua*Yo<8-6$}{Rc_IvCCgGIgH2*BGqQ$JxSK>@ zdi;{S1t*9Pm0=TP>eH1|sYD;SreoT91-M^qp+{ z5BBaDrg0?=;P~~;)h8FPzw)j;y)Fa*vJ!g;nv-Z=^P26ObXz>SfV2%wNSwe5VmmhW z*q+G*PhuP6A;H)S${x`oUEkL?HhO5A(@D2>b9_H%MSAJ4$$G}0|EGCz0MxhkTe}Cl zVxp^}-4NRih8O3pqvfEFC^ZGm)OdbU%?NjX`R&(}-Y}ewFk#u_b*BQDtA|mu+cRc>l75!#Zgf2QCd68{XD!xa|%(B!*#KR~Y z%-2>b4E@i4g14E0w?Y!j`pV%~d58!d*_Lv7w0Au2=+wk!0h=uHgZts-S+B1cmX8Qy zvn+om^+(MfV~4Rww9Y(_6g=X2j4)?9CFBZ)z2#9&s&GNif4TqsR>rmfP}}rR$ca8bfVpS*gkLEpzetLGdi*EJqpno@k!>4#tkF&fA~r z)r9^!M&hseX55mB-)iwyNq}yzNR9{?i}d(3774)b`tR)%OUw!J)jf z*ZO?X2{nWeH7RkfuQiPa$Qs!;`eK=^#1;J|bd#AT6>Nk;R|wTkN9lD>#1rXKMWO#J zjzj!S?90J^)tdrp&9Q1BG@4*vx!nE7_E}e!)r+B_-3OuTQ|^q&)s^Q&l!_?L;t7@? z51sRjWsKs~8tR5&r~@T87|6o-_5CFBgeX6;COV&$6nc_}p5N;0b^6P% z?;2~*f|w4uzP+le1CCdy2F zhltQKR!-=%$nzZQ?0l!OcGncc-G3_vyl?!{+I?=c%3ja`0Fv}+b=(P%?ISG=?v2aE z+1c&csij%Aji$DfWI~*b{1IbJd@UpNI2E0fFDL688+&^jt)q_(LA>Je{QP9m9slc4 zg3`+dfOL4=9U$wv7{P3*a=&kA+(KxAy{T>UESLEHC=v^L83$(K*(iU53vo^lK0dED z+P7U1SucR18_9buN>38GFEbJ2R5UZ=CD znD|=4ik_yUWaK!-tC=no)lkJp(^Op_>cg<#Z68AEG`RJP^F#eCi!%kq37w$tC; zyF)}-6jipOCq?B6=Un_PM0x1c=#P*0;eH$dRgk3mr=zvCRk5Z0@n~;vT}+2`X+_htewrU~FW2 zK38>Qk=M}E3PS%jpvoov@SCji2xCg~cQ)kjKYd8cz3cuOdYXNQb51cP z$VPggj#pui4S)(s`>W$24#KHJ|IOZc@V1S#377y^*Wc@?26&AC7vB(N@?l938h!$1 zd%fPv{)7DoM!Sxh4RF1=Zi{8vT3eD6$s3(zTehQEk&T6e7|xYHaxNOUh3JBt6g^lx7)pMPfp~S`mL5+NmHpTFT8Vo({pl4CSqtR z<~R=L+8Ep%r^Jn`4=~0OqJiv zgVURB(GOKZ+vO%z;!AQ;^oF2SUtg~-K9NgdsfqYn4Bc@Vv6$!6;E{>o;c60&HVNO1 z6|FugQxU^=8DqehXF9`I3=`prMPQf{Fl2N^MpzNgm6;q$0P!>xh{M#pHX(-X`C9S; zd@MnR2Qff2gcoIUOkx87Df)D(D59Sseoh8gVcq*;sZ!f-wp!UmpN666hthPRx@^;H z)ur=oVY&$l(F@*?F))FQUcP7n#N&1W@wG`Xh-16@q)e4iQx+8q0D%?0g#%(H76FF| za7_mwj4=Y>QY~jXe8ASyBXmr3Q&ZvG_iT9~VR36J2InRNuzd{=;KTO&sFe$fSkhNM<4FD{(F_}G07Ga3tK zXfPR8z}y4kYeEf%`q_5a%tJww;H!yX{vecdhoNT4nMz#%%i&5V!-C+HTshiIDgu07 z1yA5!7QNwtotr)-wykEWGePuWUV|SO6Y*`ZXqxDn3*H3LsS9?j0e66Bieh@v5%>Ay zrL{`E)%fofJ)95-zpdUEH`AXko;{!axf%43Xeu6fR4E|~JQ zipUndI#am~UbHGUEOry3lL9^}YB|}7T4C9fM8^v$QFJtt=xL(Mi}Om+sTJv>VG`M+ zxK@i^blzU5?zaA?L~jVuTU&dZJA12h&y^Xu5{wU!JZ+2uL}Yell~lY zueIc8dGOyLdXRQ6SVRkP$w^<`^+OQL(ABuzXmCp(687NLZti7s)$Ne~?|h)vT(7k>H4qDMV9Fp2C@ zTu*f7&R#68%UfNyEjr$D3vRFvTXcaG{hJ3{^JmZBn3=(#CJbpz2%k6A&N~Am-f>SC zoq+{Fe5_%TGqn`;+_*u}NkK!HrR8MD1S6ubkt90aEeT=3$3*udx4<~C6w$Haj*4!o zo^y`{lkYB!**5| z9v_c>L=(lDO{u4-W12da3jSmX9m2+IU<2GF(IJ2t<83C2Zi0k3s$OZLOVjnZ=!QC9>;Yk}X%;L1 zCKJmNMF!D_;E_X$GjBh6`k*0|>;F%PE*kco9r-h2HrM92FJ?dfAuUAd&B7>n0s@N< z%f%p;+Id$y?%71wIQW={Ntx;?WmB@jDww9r2oUq7C*y|TiSa407&C|Q$`o_5;txxZ z7POE8p$VeP{MTJV_?SK>s1K>nps3aaDWXf$i5?T(!`4_Qqe(ex67(=r6se+P+kr=7 z19Qh*Sly8AxfFeHP0@pOF~|hbgHd`vm;!7z8xJ3DtXIm*E01UI{`^o^EE`vuax+XD z++n$t)XqD{j(cX&vEZcg@k~{RP99FmaR%^mqGK0IIWfP42+?h@DTbSq6=siIF>pdR zQFMV}kNly;i-;cnW8yQ$MA329E+V=zAd~d~;@f*qw!uWG3jBce95v734DrcHA?H zZh$kfMR)K#C3Ie3cu~>4xY8zEd18rC!f7Eqb50hpU9iBxJJN}wOLVMG3nVJKqyTi3 zv@=O`G@Yu_MHDS*DwJl8;`*X%cRqc#Qj;s=OlwNh;to;u6Ns#&Gq$8rF#Che<#w5!C0tGc=!^Gs$*PVf*tqFqSN9?WIs`KtaT`{ zv@wiG#v(5zIszsx`pL-#j`R!w3ny`tM8~!hOFS+*M@?-uNpv*b8n4<-W$;r6KjdhH-0NA=4+uN+!`FrBLiB#;XuZ0){o>W=od!dh8F9l2QIyRCp4xFw5uGrP zWA*ODGZm}%Vex?i$Ba!TU36R<;mN;qvVz5BTR2hefpMkiTy5@?L{|-(B)X0z7!jS} z3wk*0A7ZoYQCwg2kDt$P)myEj>xeFqo(uZu^EiWmen_`_awz5m#Ixd8SBn0iR$pIV zT|9sOT^e0G;41d&7Tz1db?$1%J)`I<)C7wriH;d-7sAWMyu`aH(F-K3$+ev9*p@P& zwrm{oP7vKs5#4?{(TQUEm~?jRuDW^=WQrmtItl^lq7V7_c)nb33DZxe?Yaw3h3L)D z^h=*ZqdQ1{pfvA?hh5pAcLoFTK#2ZOP!GPbdVc)fS7Ub;3ScVQ$O%q_gBR(zPb)h0 zZD5IO-Tmlj4?hpTDbc~5c7UFP^IjfMW#*KT+i%f98EbL%E$WEfNEbal3LLFdJU#x8 zk+jLUdkfbVy?xYfG!FuX`#(w&TJ_5YT^`wN2*iVi*!x+~s@E&k#fzW&nPRNoq6M`X z*T+qFp`>=)rx)F^z)t?QQc1ZgyxS3-gFo#6JqNFGQc#DwKKPSGpD;#eFF8hs_|jfzZAVOz38o4>qt1;6f_#ao>50#$#DLy+uRqk>)q~w=%aTf z(rhtTCtnkt{N|&BN8dbnu(MTJn?Ha1W9K_Y{QihTh@(z$YKR)ErDYbKSTwro^Tfy>`@>TS#;|r=5DE`>?q`=pIVb4+6p(U~sxGd_N8T>^Jv!_nXan zz252UHX8e%xby1JiqY(Y2(8Swo z-uTyxKw#p_*Se36`j^jP-nMXx8h=tbplSRi}X2wLp^ch-z{$W1>@EnFeBnrihNa%wQ_m z$%5Yeg63I=bwweLgobW z-_IYkz^$rM2Au7>7u(} z`8szjaMMJ`Zj=09Ty%91K`kiC4|B7km%!p8UWeGZ=~=Puv~6cpF9~A=@Nqa6Q$@F! z1z_pw06Rr=+`s};8NCPF9LvKNeX1x*N`fu{z>kUVPifJw*L9boSE|+JvyWaJ&))sy zle^-N_$$I}@b`bt9v|PkckftmZ}#qAeel6wz53}JZ@lrrTSD@g#ED1_u1NrF22fDP zRcWG^z{55;MRbEOY|;}%r}A~Y1ptKIaEoqV3Qp~!mXirSaSGG2BbSJv;27Ff6JIq?X~ZH@0C|x`N2DHd9Q^( z2yW)8TZ{6uQf?L)^C|Xa(!1`KtqiSKI#q4Jor{|!F5`!ZY z7aUI7VTRfS?{O#LfUBj6E-6GdKf|Ub0miaNF}XIN#MV+<^t$#@yE$_jP`zOEg)n_X!%d27% zUHsV#2)gvu<(2aC;`y_;aZfmO(F3ft5P}Bq1kp|4siLdfX_7*k==eF3Ctoy!5dAhq zr_+g^gMBd_Tt39M)YoI8qvtM`P@?C=NEaP*(K(>FDJ?oapCUT8`tp2n(dBx)(yfWECZKCM86Ye&H;r8+Y%3E*rzR18 zOc0&p+KqJ4u~k*mGr{(gMb93EgtCPFqkTerEnRf|w?DsMsrLsbr&Eb8j@~$|cecd6 zv%T_KWqo5~{QY5 zn4~3-65RsNxFyjk83ykWEoZtP&q4~3t}u>%6L7- zO&&?vW>ZBG;1KXi8lsy7C*LREUi-DLcBSZp({HnhE{_W~j5;~m>}>6p_o`ye%#jei zeX=2GZya?x{YK+qb7yn2R(*0lI};P#wxYFHq*~MAhXI&RQFDfIO%&Z%7cr)b?kn?Q zev;@K^#DBI7{6S+>xquhm+;@0C&vPINjTg_G1w(G_!`(u#R7@tQ!F7i1_1%_)V{bi zdao>NOb|tvrjO34T!-OL@iHZzd`aL<6-9_4%m!3$t?XA69fAFv^wvZTf9|^kMhS0q6NGxODsn_-vzc7nz94KOj5lcgKutvw&hHJ~n zio?L7wX>4On9v?D|Eg`+7{M~tLWqs0j1?^Y6d$meXG-LO0-|4$u}mqX2`9#z{;uiD zx94QQU<#T-i6NfGqPQkEG{k2S3Z~DsD-MwQt4S%BZSrR^e2)b6>)Mx$QemwGn?8iJi2`EwzB8Zjz`+Zn6{6xT_#PM5N!*s6k3&wrCfU9l5em(z0pV^ksN59 zkSR$BU;;n_ZxgFPzz&ONygF0kehKUtgMmG-=_2j6z!V>NeyF|W_}BCLeeuiN;I|$O zbh>i?8Lw?Y3(X&7q7n8*n0cAzY3TCI6TRJ<{&<4|+HDbZrv&vY@_wH3Z3l3rDtE}ELt-fs}Hz4#zy{>d& zKHood@UPHiLSP$SEEXG$-Q$~gG5>c<9zh77&tJ0%!yF8q{xtO53GYty`9oJENvKxO zPA^V&cRoB?$IiWikUtP*HDlyB!(qRdq)T_cNr?3SgsyZ8r)Q^U<@|>)kLlyNM-ak= zI}?GbH(%JC1D{iDhQ> zGJO}i+P-Md8ahx2X_QM+iRUh_JRIi;Ay~L43>vy^o5{_sWhQoW7rF=&eU8vCR8pyz z_-u}Qf91o@5%PB7uA9!lvB$T)&Hat#(DLz7vnrfjkyZ7Ra_`W^b5J`c? zGjr&mN;3!fJohB%=|D#a2YN&|Mz-A_PAqFYhR{Rd@Ii`~$jqUGmT=x_x2hk(MLEyy zg}n?NAruI+`pB^+lS$vQ24h2uGlAH3X712iq9{~LrBb=xeIKSJmx}fL5%-JA8L3mF8P56B4g_R6sxg+QWh`PUrS*q#w6OV6JUFbrlGc3oKA-Bw33MW=v0BvP?-F~c=t@M}%g%5oXWKE#Xdm%-O{gzb}J z>xaZUn0pRr&`ug7@og!Fhtp6B{N~1VtEy52{szH0y*Ga!X>7TaU0=@u=!F6fr2C+s z{<3duW|9LP7M=jXtGAMKNq>Mv+&Lr8$pbx^oSPUOxcqTvSMQ~b!Zu8EKO(Pc`g=u{ z9c=Kb0UI?CmL5NJM8}CTl72`gjqR5b4aqAN?<9DsG*1lV_+K#@DIz)r?IEs(5MwSJ z(q$}a)rhT9IwNaoyL;*pt_m;H)NKVD5?$`St<5we8e@u1?V;2POlI;2)HxV6O>jI@ zVXhr7oym&ZE@Po;N)h-Q1n1Nkp5aL2i8OfZJK(zKadny8`}cVOz2!eHU&kbMn1Yr- z(EqpifTf3kHL%T#NPoz02GD`?g`&tkjgpF#O$(gL4brWw`{Gg+ltQ9wIva!!WByi=Oo-Vxy? zpzXq1h!!usYBQxqdX-WXzEZ>T*5}nV(*>Xtatd=SIq0A$vn8NIlTr2=@Gvv0DMgu} z<9ZW*l6wvkW?&VmO;}+jlkV#HuaU7Ce|T*ktoqW@!)Xv&^shl*l@ze(yPKz{7nf&| z^XaKuBWTc9vKw$0*?|=;nj0zsUFU7E<{si@Gn=a{)nPW4E6g1zia(sv4bXAb3P9&T zb}T`q(!JRZ1zU@w9R%oZjNZj+$X1~s6^VUO|2eQU0j_%jA9oC#FD)bJi$&1C@o@%o|M06l zpx^(-(|z1|_U_$ea%y~d=!=tsJuBJGoh>+Wwwt=?<$!MRY{!XuPz0-zSBwQ)OD_9bM^_OaVIvkY3x}+*B^;g2~AIOsOx4(-}CXseKl%#tkEkbzDXA}j+P8ku6rK?i9} z7lR(;T$O+>DMh)U*GK>9Xw$Kl)(aUA^c_^*1AS)$KtFwTbaXB`JNw`P@QoH7}Fg1X%Dgs^eMoV}|%{`#&fgv!H88Xnxa0@_> z@SK}A#*-BzYRW(tl%j0V|Fx;P?UR$;9j!eXSZksLdSP>OYim23Zg0PGZG0vPq|e@+ zon8F;>m`W3!_!g!um~ya56b~P0lxdp*u5J=S30{cWj6ryot<~vnUyq*vDNRdYGKsf z3fM7=4`#B`)^J^2h^~vqLR^ioh|F=9fDRQ$w}vnai7M6Ny$UOAVDR0Dg{|^Iw-0-w zQM^!bLzwXm0!vb(whFo0-aD}B$8E%|&ro@r$%7tYC^sCyOA0!Y9@-DO$~4-;X08^E zL}CsO>@l;NQpiF7)tS?sr%uAtkvltkvIzRlJD>DIVRHvSU+L;Ner;rODwVto|0QP^ z=a!a%>W2@(!~^@~S@dLbW^ClLZ_qaixg5@QU(aO0U1+M`U)4&aBz2$F-rd4^v#eBj zmw_A;ioEO7^Uk~6ahHJ(HK$V@iYnFSu1#EFPAn3MYA$o@$_~cuiaYVH4CWX{iFG() zrK*Wyxz?_C6mu;R=uo?%sOy?MKJdc7wRDZef_DNL=s>y~3WCl%sUf!%l(Ql(V`ep_ zkb>UO)Y8?{3%P2SItMRh)=Qx00rWil$fhA0cNtcJO~KPulAyUHdhbhLFV1@Mo|7sB z9aOUDsmZ0WzJcSdU9cJ*tRfAZZx^=LH?o&bwKmiXs#-Fr5Do3(N?(X_AvZOmu;}h} ziSy38DRtZ>pwpj6$OkHw?>gBOlrsU=@<6xDq8#qTW>GG4G=v!JvP6qO%7gaqkgyu;_)& z?QM*_mjyamRK?ymvQR=G!a~K-Y*q@tQw7Q$cRA=G7M5D{Fr6n(%!5=r2)cc^D2Ko7 z;uHH){#+*QRP-tc}h1_LR0J=$-Dw+&* zBHh`0Ymac9&)DBo3OVTY4M&>WdwSDZ7%~UKVFh-SDd;hSaaUClRVtkYyb9Uyfn}0H6@hNC2qpd0 zRGBQ!>x1S(PPqVd!kfIwAm|{4E|ekxofHq;+VN|22HZx{swjmRbmQ-iG`5_C$!b|x z2XbQ_T=z~7bh79``a1v}eD~JIrnW~{?pzzWejS2xsT5$IOePk=s{d}z&y9uug`lHg z@6`On$iVTI7I+l1my3hS7hnW$15J7WbP;WQskm~LB99aqiTVLif{mhN631N*x~thL z-QL53O66|vnj#oEkc^j<1v=$M*!Wt$Y6>!-IK0(RMW6>+VG$ANydN$DJw#V90J=sW zg#2#*t`x0`QV2n>|F>hUr%$GF9mpN%G;eGG=obL=&Ap)Sya&*`n;ZZ9k4Mg2eEj+N z$gTPLR4SR8!l>Nb+|8Re5p*!=m>E}^?SgsmMbHzT=BGvnuEI0ie$$(Q4!Wl;1ii2U zSu-7I(&eCAu12k$b6;%AY9J5Ep|KIlbC){qa?p8MbxgaeezNSL8p*XQ%lQ<-d>nqvX$Gy zeAEnrzJj3V^1MZVpWlFh+0hrz{&c?SkNpkPkX!!53Zk2fy>QYtg?c zfj&8L?Th0rjV&kAnVl^}9ql^y4c5VTKP3abgg3Qzieb>mLX6dM*c8(eDf|_1+^YrM z(iNr&bKUh|Z;wi29u(*-TI--!D{3=nCMa^Mxxc(cXIx*)0gH9Hv)2lB|eRQqNJ~|VmMw9DKUkQL-MJdc6=+HYq)xDC*qT}wl(K#XL_{(N~8*|c+ zJ$tkI^4ouII&=K98+}6y<6zbAr&70XPfn*mfcW(Evi}O2^AYzoGc&h{*{>fpKYISC zu?u{N9O@mOc?Zlp1l~_KHpoFIpu;T0$KDi$Aava@AK(l|vC7LH74qg7dSFhA%yrka zUk3CL)2Ut7RFP<&M4Z*(z-EaU#Snwex3ah}>Q0=2X_i}b0qGQsBTriny1W!Y(4Ck! zsD@Fo^^HyMK@B+^L<7(VJ0aB_HXz=pJje@>XCX4){B-rlAAfrG+qUL&$G_|E8yXr$ zyFQfw+n!9N0PtD-hcjM43GWpthaWSr3f0*7@OS6VKmX}DEMbGa1GMKrI)V;Qc!Tsf zAYB%9rwKcb;-!R!Y2bsZL8JN%%<$qs*Qx-WF*-7yTON#yxl+*8I+9+Q>u%$IRj4Kk zy2Y5UIE|5rLAUrGv=sn7=qE>|pldpFM4;|`^}G*y2G);j`k541 zeQ(e$#C$oWG3yv^jTf z{rw~305PP)LPtD_u+J?e5(GK~>Octk!q9h*+Mcey`SHnlST}wHN%v2b^CJFe(q)-( z)Vx)*s6BIcOlFuxkKLdT$^kv@GLRepV8V>fqI5|_pvM#f5(5NqlOoV1DX`^(9>;8G zml?HAdc^|cD@#EUfF6nwns~*6pj$eHf$-ih2c1@#;X$W<0|<;h-%Z!Fs!Cy(oca2* zEgd}=T;JIO<#Jv+doG8dV`AL)9?iegU>W{Xa`Srys zkMHzf9vK-OogbS?An3`Y-zhJFj--#ldUMzBTx@#u4o?AR_JRO| zjSUjeT}{&Yiuy*&>zy;3QPeA$^mm)bB~hVS{Y9UgSYa2Rt!d|Ij;bXvfI2fg?mx{tetZ4l^Hl!9Wy z4NZ{pnpw|*IS1vu#a;nx1pPwiU>ZROBVV%UFpiG9t;46Se|-7+_3JhK_vYpEKmGaq z`SWMbee~&IR-VGAuHUadW)}wsQIEPCDsH71n&Banial(&cFg`!cl7r4CrQ0n0A<$LV zAd@Zt-NMgl2!J_8-NAyc6alx$Ju6Q&nbHuZqXe1=C!(*JO1DrClPpTXlC3afDK3q1ixl>) ziyU-NEU@?)nHp)wvHVh$nn^vF^B$O0vg3pPu7IHDb07e{*9RRJn4w@CI@hpsVPmDc z9esD;e9hC_wKuEZKY9M`i%*)`&Yt`K_Z)6lSJPaoO-tfq+gLDkQvFCgeRQs2{f#Gp7eiADJ7sE0`*in{Ua1eU3g zgHD^_9CFb4h@dHS)o7w2=|DiIaYY(tD_=G7OEV~pRC3~6Ljt z9gq6%a?r~y1sb?e{a@QVdzdz|wU586S16=IN*VILDJZr{la&%hLR;hpvNxt#Zjr_k z4Gqoq4Lv1^#3m88SIVuZx8J@?X>B&%B?$z~d@*M}&KY~=%zgf6#`qYIZya=k#MSvl zfUzh3$`WafBWZeztI={^6Tr@@7}%p^Y>piSKc1G%yw-1c(dMU zhglqpLUc)@b4h*v^`-yU(r3*@dg{Zo){sT5i=skwyzSMZSC`XVy#G1T)$^F#`GVvg zrhh%rZ(T%mgO-b7>FNA{nFspqlSD_6R{ayI@Eieu8qrminRBA!uj+nvp}~v13h>~Y zRjT^X2_te$^h6@^9xRivKJ7`W>I~6kQbzQ+5FLpe zPLKAf$-c6*@@#v1b93|gn?HVdO#JZIA8(#N-`w1O_-yCd(x+oI?x~G_`le1Nn<$p5 z03{wolJ6w?dabszTqU|TFX8^;{D9&5UAkQtT`!3|bwtPYpAlV)`f#;Ebj-(l_?1N0 z?!ct(7cL=%b9F_EgjiWE`mJk=IIB(;ef|cCKzT4x--OPCJPo4f1v$F7I&IM4Px&pENXbQ!-oo_%_B`1aMa zwUye^7>{nw-v0Rhi2keV_@2}Kq23_T(H8*Ei=p&fpgXQs``g_L(J5kUFJ(LgV>#FP zPw62#IX)}8-i>*E=Ch*Xn8{Nf%jGl+vtLQ{TZ@AikNHrB3vJJ`c)uzYM(cM>bhL!; zEuvJ=MA7HxZm11CrWIYAnvKJ?x??lf{`cHH&s@LAt3YpieH|%tqGrj!@IqtR%b|Ln zgvAV440~~iK0pwO%%XsD_w6IZbIW&pABuYpVc8B`_q#4d_JhO2!w+xYzyI{<)vHhM z-@g6ufvz~%KRW9AUCTu};B_C@HgLWXh~>Q;Ky+2z;>pU=-Eq-p9*m#8ptqLFenX>c z+GZCIZ(!_W3eo5ENsONpUGM$Jr3%qe;P3l|x!KV_mxcd?=y>%Hi-*9^=`qk^#*(7B zJrrYb{S1AaG-jhghk^%lS~<2GBNP)b7W7>F!P);+qMzL%iLRM`_LzF%D7r7IEejTK z6R~iiSFv29Q7!UIqGY=On#)|xPAf#0oR5hPD5lc}N{1B7Er--*Fr7lX&lvN)qKnRQ zEV|OQ-EQ~v^!W6&OBeCGrOpe?X3X|!%FTz`Pt7pmeRL5BNKWunX4LDyvAwoNI|+{P z{ddDROq;(By&s$%Gc$W{1_N66)f-iIck5{PF(UK=oggNaVY|LK{7eU@(wX5yp8H_+ zU3}=#7OnvtNZ>3!KRj9!UHgzGKd?6FfA8ADBL4gI7-p|)jHG?q^$QqO8|B`eV&0iP zsHSuO&Ky4W$^)RkeplL`ZS`}Rom-$nq5yj>-kH07p;vKlxwg8|#8Neh6{5Q40G;r| zh^~vS5Jm8(1M0837V|C3rth`~NNClS z9Q|N5X!kwD13-U4cjk*t-}IL;`5`7=3+7Q&_I~y3D1+f9tzl_EbCh3&SrA!+6A>bM zqZ4s0qU#6|g^jHz`^;s`aV;q80j7i|3Y{HXE+l7GVLiSq3!>Z1a=P~Z&em=_N_sh( z57cU)`-bQd&NLf;|MBV4qf4F~d0T6(Us()>t>+vt~5O?7-z0i4;cjfOkScYFpc@8sn8O>Kij=e$VWF~}#AoO1V|wYS%3 z@2VxMmqxMZ^x@Sn+hQ=Dqj?_p`zhz8=gQ+)i`G1e0*D?=5FK#r%~U(=+O~ZH(Jfu{ zGDiovrp(=Ko3Y|opFl*ny%V)kwG(j+?hQoedCIXP%+GsGis`G5E^*}LI&8;a+!e-P zJYT78HL!ar6i?IAb0MU@=K7$8AjON93gtcXTq}-ehDskegUi= zfmw+0zHPaOYwNq6EGOmsbOCwM|3cXw;;&@Obg3Yexk&{QwJ3K))u>9)@fup~W7 zxk%1ZcY)JT9JF5SHJZdvwaYLa>jG!)VO;J?k5?HCNZWT?DAmsUWiW^2m7;f4L>K)+ z^z#jLwQd|^?W>=MXD8|rxbxZjn!udm?bk5 z-Ls~{f#{a)`S#IA)$Ps)qSJHBA$pb}cW*R1S)}Sve0643DV{5h!JyWjH#?yeX__X9 zME0$<&c`BpqloB}Mdy7^-EsBzr>qcNhxtWR*BJT5$4DDQcU|lFO&yb9(}C!`6g|z< z!kT6v`q|5=JLC)of#iZGL(x;rcQ1=y(3Qb7&2)po@QAwGo7kU0g-bFW8T`1z zpxC5A?Y!3OSB|ZTLiN6+q`H2H&U44MPnd;iGfBbX&tNc~Z>iaEMYf&;A;I~~JuXErQ}<>tE_!?>dJm$jtsGq2MPr*0K1827qC388 z*=VFUI$6efPYlQvTn+NQ45K&;!eZNWgTXM_^Ox;T5M`3KI8{+;K|FiX@Hh7`~RBq9pUtCDx*<)!E% z*AI+!(N($FB#GMA1#-t`gy&n+<5}p&vkq(Ps3w(+ny&Vf=rpL29>!_fPiWA7 z$t3RvLnX^gtC%LEn&%?dqIhLI2O{SqqE`*0i=GlaPh{AkDv^I3do~J65!M&sTRyX$ zgW7tl5S{!Uik@TP3>xX}ASLUgR%7B!ScAdzw=2&HiFt67i3i_EbXghEM<_&(vrdba zr60IR#Tnsy@ONs_ZObRoSGIOL*eW6)ieAiy%>va(4`trxG5X_H#$KVpV8E(WcrKOb zJ<*$jVwq|wdcTL&qhdVnBe0W-G78(x7xl--tOSlvkF7-CU#stSvT@OI?;Tp$(r(E- z#fERYl|?8F2Gif(Kv!1E3(<1{S>vj3t~JunL~jSOFEr6L*Xf6Z=xH2w+KsL4eU~w- zC>mo;j}OtAwZBtuDAD<-=t4%w-Kk4H;;E#I&B|qL27~cD46_8)WJHqm&Q)?Y*GTk);9Lx#`%kb_1Vj&_L?%fT(P&%Uw9*ZRQP1l%Pdt$T;%JN1sw~&*KB6>Z z0kHs1|3ir`3ef>w)DM&@^7eQp$-li->UhV3mM?`gx2@xk1JO|hLm?IPq?PEA5OJ1B ziP1I_(hUZkM>P~(BqJecwdmE#3(kugtnDxs6%oBSOiGBJ^F$)0u0Qz`RioI&MGutd zATLEvBwZ{;$M%m9y}fQ)=>~(&-Q}H~ohKJ%XJ>6~XX;~jmY3&WU7TCXlOJmIdC()> d{U5^>;a@I;^>Whsgn0k}002ovPDHLkV1flI6HfpD literal 0 HcmV?d00001 diff --git a/img/websockets-app/.DS_Store b/img/websockets-app/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 - - + +
    @@ -61,7 +61,7 @@

    Learn more

    How does it work? 🧐

    Given a simple .wasp configuration file that describes the high-level details of your web app, and .js(x)/.css/..., source files with your unique logic, Wasp compiler generates the full source of your web app in the target stack: front-end, back-end and deployment.

    This unique approach is what makes Wasp "smart" and gives it its super powers!

    Simple config language

    Declaratively describe high-level details of your app.

    Learn more

    Wasp CLI

    All the handy commands at your fingertips.

    Learn more

    React / Node.js / Prisma

    You are still writing 90% of the code in your favorite technologies.

    Arrivederci boilerplate

    Write only the code that matters, let Wasp handle the rest.

    Learn more
    React

    Show, don't tell.

    Take a look at examples - see how things work and get inspired for your next project.

    Todo App (TypeScript) ✅

    A famous To-Do list app, implemented in TypeScript.

    wasp GitHub profile picturewasp

    CoverLetterGPT 🤖

    Generate cover letters based on your CV and the job description. Powered by ChatGPT.

    vincanger GitHub profile picturevincanger

    Realtime voting via WebSockets 🔌

    A realtime, websockets-powered voting app built with Wasp and TypeScript.

    wasp GitHub profile picturewasp

    Stay up to date 📬

    Be the first to know when we ship new features and updates!

    🚧 Roadmap 🚧

    Work on Wasp never stops: get a glimpse of what is coming next!

    Right behind the corner
    • Improve Prisma support (more features, IDE) 
      641
    • Add TS eDSL, next to Wasp DSL 
      551
    • Make Wasp Auth usable in external services 
      1973
    • Add more social providers to Wasp Auth 
      2016
    • Support for SSR / SSG 
      911
    • Full-Stack Modules (aka FSMs: think RoR Engines)
    Further down the road
    • Multiple targets (e.g. mobile) 
      1088
    • Automatic generation of API for Operations 
      863
    • Top-level data schema 
      642
    • Complex arch (multiple servers, clients, serverless)
    • Polyglot (Python, Rust, Go, ...) 
      1940
    • Multiple frontend libraries (Vue, Svelte, ...)

    Frequently asked questions

    For anything not covered here, join our Discord!

    - - + + \ No newline at end of file diff --git a/search.html b/search.html index e6fa71b17a..8ccaadb070 100644 --- a/search.html +++ b/search.html @@ -19,13 +19,13 @@ - - + +

    Search the documentation

    - - + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index e04e07ee38..bcf7dfd387 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://wasp-lang.dev/blogweekly0.5https://wasp-lang.dev/blog/2019/09/01/hello-waspweekly0.5https://wasp-lang.dev/blog/2021/02/23/journey-to-ycombinatorweekly0.5https://wasp-lang.dev/blog/2021/03/02/wasp-alphaweekly0.5https://wasp-lang.dev/blog/2021/04/29/discord-bot-introductionweekly0.5https://wasp-lang.dev/blog/2021/09/01/haskell-forall-tutorialweekly0.5https://wasp-lang.dev/blog/2021/11/21/seed-roundweekly0.5https://wasp-lang.dev/blog/2021/11/22/fundraising-learningsweekly0.5https://wasp-lang.dev/blog/2021/12/02/waspelloweekly0.5https://wasp-lang.dev/blog/2021/12/21/shayne-introweekly0.5https://wasp-lang.dev/blog/2022/01/27/waspleauweekly0.5https://wasp-lang.dev/blog/2022/05/31/filip-introweekly0.5https://wasp-lang.dev/blog/2022/06/01/gitpod-hackathon-guideweekly0.5https://wasp-lang.dev/blog/2022/06/15/jobs-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-futureweekly0.5https://wasp-lang.dev/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joiningweekly0.5https://wasp-lang.dev/blog/2022/08/26/how-and-why-i-got-started-with-haskellweekly0.5https://wasp-lang.dev/blog/2022/09/02/how-to-get-started-with-haskell-in-2022weekly0.5https://wasp-lang.dev/blog/2022/09/05/dev-excuses-app-tutrialweekly0.5https://wasp-lang.dev/blog/2022/09/29/journey-to-1000-gh-starsweekly0.5https://wasp-lang.dev/blog/2022/10/28/farnance-hackathon-winnerweekly0.5https://wasp-lang.dev/blog/2022/11/15/auth-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/16/alpha-testing-program-post-mortemweekly0.5https://wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/17/hacktoberfest-wrap-upweekly0.5https://wasp-lang.dev/blog/2022/11/26/erlis-amicus-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/michael-curry-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/wasp-beta-launch-weekweekly0.5https://wasp-lang.dev/blog/2022/11/28/why-we-chose-prismaweekly0.5https://wasp-lang.dev/blog/2022/11/29/permissions-in-web-appsweekly0.5https://wasp-lang.dev/blog/2022/11/29/typescript-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/29/wasp-betaweekly0.5https://wasp-lang.dev/blog/2022/11/30/optimistic-update-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/12/01/beta-ide-improvementsweekly0.5https://wasp-lang.dev/blog/2022/12/08/fast-fullstack-chatgptweekly0.5https://wasp-lang.dev/blog/2023/01/11/betathon-reviewweekly0.5https://wasp-lang.dev/blog/2023/01/18/wasp-beta-update-decweekly0.5https://wasp-lang.dev/blog/2023/01/31/wasp-beta-launch-reviewweekly0.5https://wasp-lang.dev/blog/2023/02/02/no-best-frameworkweekly0.5https://wasp-lang.dev/blog/2023/02/14/amicus-indiehacker-interviewweekly0.5https://wasp-lang.dev/blog/2023/02/21/junior-developer-misconceptionsweekly0.5https://wasp-lang.dev/blog/2023/03/02/wasp-beta-update-febweekly0.5https://wasp-lang.dev/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hearweekly0.5https://wasp-lang.dev/blog/2023/03/08/building-a-full-stack-app-supabase-vs-waspweekly0.5https://wasp-lang.dev/blog/2023/03/17/new-react-docs-pretend-spas-dont-existweekly0.5https://wasp-lang.dev/blog/2023/04/11/wasp-launch-week-twoweekly0.5https://wasp-lang.dev/blog/2023/04/12/auth-uiweekly0.5https://wasp-lang.dev/blog/2023/04/13/db-start-and-seedweekly0.5https://wasp-lang.dev/blog/2023/04/17/How-I-Built-CoverLetterGPTweekly0.5https://wasp-lang.dev/blog/2023/04/27/wasp-hackathon-twoweekly0.5https://wasp-lang.dev/blog/2023/05/19/hackathon-2-reviewweekly0.5https://wasp-lang.dev/blog/2023/06/07/wasp-beta-update-may-23weekly0.5https://wasp-lang.dev/blog/2023/06/22/wasp-launch-week-threeweekly0.5https://wasp-lang.dev/blog/2023/06/27/build-your-own-twitter-agent-langchainweekly0.5https://wasp-lang.dev/blog/2023/06/28/what-can-you-build-with-waspweekly0.5https://wasp-lang.dev/blog/2023/06/29/new-wasp-lspweekly0.5https://wasp-lang.dev/blog/2023/06/30/tutorial-jamweekly0.5https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/08/01/smol-ai-vs-wasp-aiweekly0.5https://wasp-lang.dev/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescriptweekly0.5https://wasp-lang.dev/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-aiweekly0.5https://wasp-lang.dev/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-callweekly0.5https://wasp-lang.dev/blog/2023/10/04/contributing-open-source-land-a-jobweekly0.5https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programmingweekly0.5https://wasp-lang.dev/blog/2023/10/13/wasp-launch-week-fourweekly0.5https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wslweekly0.5https://wasp-lang.dev/blog/2023/12/05/writing-rfcsweekly0.5https://wasp-lang.dev/blog/2024/01/23/wasp-launch-week-fiveweekly0.5https://wasp-lang.dev/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejsweekly0.5https://wasp-lang.dev/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-codeweekly0.5https://wasp-lang.dev/blog/2024/05/22/how-to-get-a-web-dev-job-2024weekly0.5https://wasp-lang.dev/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yetweekly0.5https://wasp-lang.dev/blog/2024/07/03/building-selling-saas-in-5-monthsweekly0.5https://wasp-lang.dev/blog/2024/07/15/wasp-launch-week-sixweekly0.5https://wasp-lang.dev/blog/archiveweekly0.5https://wasp-lang.dev/blog/tagsweekly0.5https://wasp-lang.dev/blog/tags/acquireweekly0.5https://wasp-lang.dev/blog/tags/agentweekly0.5https://wasp-lang.dev/blog/tags/aiweekly0.5https://wasp-lang.dev/blog/tags/authweekly0.5https://wasp-lang.dev/blog/tags/boilerplateweekly0.5https://wasp-lang.dev/blog/tags/careerweekly0.5https://wasp-lang.dev/blog/tags/chakraweekly0.5https://wasp-lang.dev/blog/tags/chatgptweekly0.5https://wasp-lang.dev/blog/tags/clean-codeweekly0.5https://wasp-lang.dev/blog/tags/cssweekly0.5https://wasp-lang.dev/blog/tags/databaseweekly0.5https://wasp-lang.dev/blog/tags/discordweekly0.5https://wasp-lang.dev/blog/tags/expressweekly0.5https://wasp-lang.dev/blog/tags/featureweekly0.5https://wasp-lang.dev/blog/tags/frameworkweekly0.5https://wasp-lang.dev/blog/tags/full-stackweekly0.5https://wasp-lang.dev/blog/tags/fullstackweekly0.5https://wasp-lang.dev/blog/tags/function-callingweekly0.5https://wasp-lang.dev/blog/tags/generateweekly0.5https://wasp-lang.dev/blog/tags/githubweekly0.5https://wasp-lang.dev/blog/tags/gitpodweekly0.5https://wasp-lang.dev/blog/tags/gptweekly0.5https://wasp-lang.dev/blog/tags/hackweekly0.5https://wasp-lang.dev/blog/tags/hackathonweekly0.5https://wasp-lang.dev/blog/tags/hacktoberfestweekly0.5https://wasp-lang.dev/blog/tags/haskellweekly0.5https://wasp-lang.dev/blog/tags/hiringweekly0.5https://wasp-lang.dev/blog/tags/indie-hackerweekly0.5https://wasp-lang.dev/blog/tags/interviewweekly0.5https://wasp-lang.dev/blog/tags/javascriptweekly0.5https://wasp-lang.dev/blog/tags/jobweekly0.5https://wasp-lang.dev/blog/tags/jobsweekly0.5https://wasp-lang.dev/blog/tags/junior-developersweekly0.5https://wasp-lang.dev/blog/tags/langchainweekly0.5https://wasp-lang.dev/blog/tags/languageweekly0.5https://wasp-lang.dev/blog/tags/laravelweekly0.5https://wasp-lang.dev/blog/tags/launch-weekweekly0.5https://wasp-lang.dev/blog/tags/mageweekly0.5https://wasp-lang.dev/blog/tags/marketingweekly0.5https://wasp-lang.dev/blog/tags/memeweekly0.5https://wasp-lang.dev/blog/tags/mlweekly0.5https://wasp-lang.dev/blog/tags/new-hireweekly0.5https://wasp-lang.dev/blog/tags/nodeweekly0.5https://wasp-lang.dev/blog/tags/nodejsweekly0.5https://wasp-lang.dev/blog/tags/open-sourceweekly0.5https://wasp-lang.dev/blog/tags/openaiweekly0.5https://wasp-lang.dev/blog/tags/optimisticweekly0.5https://wasp-lang.dev/blog/tags/pernweekly0.5https://wasp-lang.dev/blog/tags/prdweekly0.5https://wasp-lang.dev/blog/tags/prismaweekly0.5https://wasp-lang.dev/blog/tags/product-requirementweekly0.5https://wasp-lang.dev/blog/tags/product-updateweekly0.5https://wasp-lang.dev/blog/tags/programmingweekly0.5https://wasp-lang.dev/blog/tags/railsweekly0.5https://wasp-lang.dev/blog/tags/reactweekly0.5https://wasp-lang.dev/blog/tags/real-timeweekly0.5https://wasp-lang.dev/blog/tags/redditweekly0.5https://wasp-lang.dev/blog/tags/saa-sweekly0.5https://wasp-lang.dev/blog/tags/saasweekly0.5https://wasp-lang.dev/blog/tags/showcaseweekly0.5https://wasp-lang.dev/blog/tags/solopreneurweekly0.5https://wasp-lang.dev/blog/tags/startupweekly0.5https://wasp-lang.dev/blog/tags/startupsweekly0.5https://wasp-lang.dev/blog/tags/state-of-jsweekly0.5https://wasp-lang.dev/blog/tags/stripeweekly0.5https://wasp-lang.dev/blog/tags/supabaseweekly0.5https://wasp-lang.dev/blog/tags/techweekly0.5https://wasp-lang.dev/blog/tags/tech-careerweekly0.5https://wasp-lang.dev/blog/tags/tutorialweekly0.5https://wasp-lang.dev/blog/tags/typescriptweekly0.5https://wasp-lang.dev/blog/tags/updateweekly0.5https://wasp-lang.dev/blog/tags/updatesweekly0.5https://wasp-lang.dev/blog/tags/waspweekly0.5https://wasp-lang.dev/blog/tags/wasp-aiweekly0.5https://wasp-lang.dev/blog/tags/web-devweekly0.5https://wasp-lang.dev/blog/tags/web-developmentweekly0.5https://wasp-lang.dev/blog/tags/webdevweekly0.5https://wasp-lang.dev/blog/tags/websocketsweekly0.5https://wasp-lang.dev/blog/tags/windowsweekly0.5https://wasp-lang.dev/blog/tags/wslweekly0.5https://wasp-lang.dev/searchweekly0.5https://wasp-lang.dev/docs/0.11.8weekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/emailweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/uiweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.11.8/contactweekly0.5https://wasp-lang.dev/docs/0.11.8/contributingweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.11.8/editor-setupweekly0.5https://wasp-lang.dev/docs/0.11.8/general/cliweekly0.5https://wasp-lang.dev/docs/0.11.8/general/languageweekly0.5https://wasp-lang.dev/docs/0.11.8/project/client-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.11.8/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.11.8/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.11.8/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.11.8/project/server-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.11.8/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.11.8/project/testingweekly0.5https://wasp-lang.dev/docs/0.11.8/quick-startweekly0.5https://wasp-lang.dev/docs/0.11.8/telemetryweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.11.8/visionweekly0.5https://wasp-lang.dev/docs/0.11.8/writingguideweekly0.5https://wasp-lang.dev/docs/0.12.0weekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/emailweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/uiweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.12.0/contactweekly0.5https://wasp-lang.dev/docs/0.12.0/contributingweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.12.0/editor-setupweekly0.5https://wasp-lang.dev/docs/0.12.0/general/cliweekly0.5https://wasp-lang.dev/docs/0.12.0/general/languageweekly0.5https://wasp-lang.dev/docs/0.12.0/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/0.12.0/project/client-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.12.0/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.12.0/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.12.0/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.12.0/project/server-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.12.0/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.12.0/project/testingweekly0.5https://wasp-lang.dev/docs/0.12.0/quick-startweekly0.5https://wasp-lang.dev/docs/0.12.0/telemetryweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.12.0/visionweekly0.5https://wasp-lang.dev/docs/0.12.0/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/0.12.0/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/0.12.0/writingguideweekly0.5https://wasp-lang.dev/docs/0.13.0weekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/accessing-app-configweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/emailweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/keycloakweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/uiweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.13.0/contactweekly0.5https://wasp-lang.dev/docs/0.13.0/contributingweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.13.0/editor-setupweekly0.5https://wasp-lang.dev/docs/0.13.0/general/cliweekly0.5https://wasp-lang.dev/docs/0.13.0/general/languageweekly0.5https://wasp-lang.dev/docs/0.13.0/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/0.13.0/migrate-from-0-12-to-0-13weekly0.5https://wasp-lang.dev/docs/0.13.0/project/client-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.13.0/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.13.0/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.13.0/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.13.0/project/server-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.13.0/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.13.0/project/testingweekly0.5https://wasp-lang.dev/docs/0.13.0/quick-startweekly0.5https://wasp-lang.dev/docs/0.13.0/telemetryweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.13.0/visionweekly0.5https://wasp-lang.dev/docs/0.13.0/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/0.13.0/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/0.13.0/writingguideweekly0.5https://wasp-lang.dev/docsweekly0.5https://wasp-lang.dev/docs/advanced/accessing-app-configweekly0.5https://wasp-lang.dev/docs/advanced/apisweekly0.5https://wasp-lang.dev/docs/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/advanced/emailweekly0.5https://wasp-lang.dev/docs/advanced/jobsweekly0.5https://wasp-lang.dev/docs/advanced/linksweekly0.5https://wasp-lang.dev/docs/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/auth/auth-hooksweekly0.5https://wasp-lang.dev/docs/auth/emailweekly0.5https://wasp-lang.dev/docs/auth/entitiesweekly0.5https://wasp-lang.dev/docs/auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/social-auth/discordweekly0.5https://wasp-lang.dev/docs/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/auth/social-auth/keycloakweekly0.5https://wasp-lang.dev/docs/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/uiweekly0.5https://wasp-lang.dev/docs/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/contactweekly0.5https://wasp-lang.dev/docs/contributingweekly0.5https://wasp-lang.dev/docs/data-model/backendsweekly0.5https://wasp-lang.dev/docs/data-model/crudweekly0.5https://wasp-lang.dev/docs/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/data-model/prisma-fileweekly0.5https://wasp-lang.dev/docs/editor-setupweekly0.5https://wasp-lang.dev/docs/general/cliweekly0.5https://wasp-lang.dev/docs/general/languageweekly0.5https://wasp-lang.dev/docs/general/typescriptweekly0.5https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13weekly0.5https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14weekly0.5https://wasp-lang.dev/docs/project/client-configweekly0.5https://wasp-lang.dev/docs/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/project/customizing-appweekly0.5https://wasp-lang.dev/docs/project/dependenciesweekly0.5https://wasp-lang.dev/docs/project/env-varsweekly0.5https://wasp-lang.dev/docs/project/server-configweekly0.5https://wasp-lang.dev/docs/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/project/static-assetsweekly0.5https://wasp-lang.dev/docs/project/testingweekly0.5https://wasp-lang.dev/docs/quick-startweekly0.5https://wasp-lang.dev/docs/telemetryweekly0.5https://wasp-lang.dev/docs/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/tutorial/authweekly0.5https://wasp-lang.dev/docs/tutorial/createweekly0.5https://wasp-lang.dev/docs/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/visionweekly0.5https://wasp-lang.dev/docs/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/writingguideweekly0.5https://wasp-lang.dev/weekly0.5 \ No newline at end of file +https://wasp-lang.dev/blogweekly0.5https://wasp-lang.dev/blog/2019/09/01/hello-waspweekly0.5https://wasp-lang.dev/blog/2021/02/23/journey-to-ycombinatorweekly0.5https://wasp-lang.dev/blog/2021/03/02/wasp-alphaweekly0.5https://wasp-lang.dev/blog/2021/04/29/discord-bot-introductionweekly0.5https://wasp-lang.dev/blog/2021/09/01/haskell-forall-tutorialweekly0.5https://wasp-lang.dev/blog/2021/11/21/seed-roundweekly0.5https://wasp-lang.dev/blog/2021/11/22/fundraising-learningsweekly0.5https://wasp-lang.dev/blog/2021/12/02/waspelloweekly0.5https://wasp-lang.dev/blog/2021/12/21/shayne-introweekly0.5https://wasp-lang.dev/blog/2022/01/27/waspleauweekly0.5https://wasp-lang.dev/blog/2022/05/31/filip-introweekly0.5https://wasp-lang.dev/blog/2022/06/01/gitpod-hackathon-guideweekly0.5https://wasp-lang.dev/blog/2022/06/15/jobs-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-futureweekly0.5https://wasp-lang.dev/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joiningweekly0.5https://wasp-lang.dev/blog/2022/08/26/how-and-why-i-got-started-with-haskellweekly0.5https://wasp-lang.dev/blog/2022/09/02/how-to-get-started-with-haskell-in-2022weekly0.5https://wasp-lang.dev/blog/2022/09/05/dev-excuses-app-tutrialweekly0.5https://wasp-lang.dev/blog/2022/09/29/journey-to-1000-gh-starsweekly0.5https://wasp-lang.dev/blog/2022/10/28/farnance-hackathon-winnerweekly0.5https://wasp-lang.dev/blog/2022/11/15/auth-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/16/alpha-testing-program-post-mortemweekly0.5https://wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/17/hacktoberfest-wrap-upweekly0.5https://wasp-lang.dev/blog/2022/11/26/erlis-amicus-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/michael-curry-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/wasp-beta-launch-weekweekly0.5https://wasp-lang.dev/blog/2022/11/28/why-we-chose-prismaweekly0.5https://wasp-lang.dev/blog/2022/11/29/permissions-in-web-appsweekly0.5https://wasp-lang.dev/blog/2022/11/29/typescript-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/29/wasp-betaweekly0.5https://wasp-lang.dev/blog/2022/11/30/optimistic-update-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/12/01/beta-ide-improvementsweekly0.5https://wasp-lang.dev/blog/2022/12/08/fast-fullstack-chatgptweekly0.5https://wasp-lang.dev/blog/2023/01/11/betathon-reviewweekly0.5https://wasp-lang.dev/blog/2023/01/18/wasp-beta-update-decweekly0.5https://wasp-lang.dev/blog/2023/01/31/wasp-beta-launch-reviewweekly0.5https://wasp-lang.dev/blog/2023/02/02/no-best-frameworkweekly0.5https://wasp-lang.dev/blog/2023/02/14/amicus-indiehacker-interviewweekly0.5https://wasp-lang.dev/blog/2023/02/21/junior-developer-misconceptionsweekly0.5https://wasp-lang.dev/blog/2023/03/02/wasp-beta-update-febweekly0.5https://wasp-lang.dev/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hearweekly0.5https://wasp-lang.dev/blog/2023/03/08/building-a-full-stack-app-supabase-vs-waspweekly0.5https://wasp-lang.dev/blog/2023/03/17/new-react-docs-pretend-spas-dont-existweekly0.5https://wasp-lang.dev/blog/2023/04/11/wasp-launch-week-twoweekly0.5https://wasp-lang.dev/blog/2023/04/12/auth-uiweekly0.5https://wasp-lang.dev/blog/2023/04/13/db-start-and-seedweekly0.5https://wasp-lang.dev/blog/2023/04/17/How-I-Built-CoverLetterGPTweekly0.5https://wasp-lang.dev/blog/2023/04/27/wasp-hackathon-twoweekly0.5https://wasp-lang.dev/blog/2023/05/19/hackathon-2-reviewweekly0.5https://wasp-lang.dev/blog/2023/06/07/wasp-beta-update-may-23weekly0.5https://wasp-lang.dev/blog/2023/06/22/wasp-launch-week-threeweekly0.5https://wasp-lang.dev/blog/2023/06/27/build-your-own-twitter-agent-langchainweekly0.5https://wasp-lang.dev/blog/2023/06/28/what-can-you-build-with-waspweekly0.5https://wasp-lang.dev/blog/2023/06/29/new-wasp-lspweekly0.5https://wasp-lang.dev/blog/2023/06/30/tutorial-jamweekly0.5https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/08/01/smol-ai-vs-wasp-aiweekly0.5https://wasp-lang.dev/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescriptweekly0.5https://wasp-lang.dev/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-aiweekly0.5https://wasp-lang.dev/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-callweekly0.5https://wasp-lang.dev/blog/2023/10/04/contributing-open-source-land-a-jobweekly0.5https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programmingweekly0.5https://wasp-lang.dev/blog/2023/10/13/wasp-launch-week-fourweekly0.5https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wslweekly0.5https://wasp-lang.dev/blog/2023/12/05/writing-rfcsweekly0.5https://wasp-lang.dev/blog/2024/01/23/wasp-launch-week-fiveweekly0.5https://wasp-lang.dev/blog/2024/01/30/open-saas-free-open-source-starter-react-nodejsweekly0.5https://wasp-lang.dev/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-codeweekly0.5https://wasp-lang.dev/blog/2024/05/22/how-to-get-a-web-dev-job-2024weekly0.5https://wasp-lang.dev/blog/2024/05/29/why-we-dont-have-laravel-for-javascript-yetweekly0.5https://wasp-lang.dev/blog/2024/07/03/building-selling-saas-in-5-monthsweekly0.5https://wasp-lang.dev/blog/2024/07/15/wasp-launch-week-sixweekly0.5https://wasp-lang.dev/blog/2024/08/13/how-to-add-auth-with-lucia-to-your-react-nextjs-appweekly0.5https://wasp-lang.dev/blog/archiveweekly0.5https://wasp-lang.dev/blog/tagsweekly0.5https://wasp-lang.dev/blog/tags/acquireweekly0.5https://wasp-lang.dev/blog/tags/agentweekly0.5https://wasp-lang.dev/blog/tags/aiweekly0.5https://wasp-lang.dev/blog/tags/authweekly0.5https://wasp-lang.dev/blog/tags/boilerplateweekly0.5https://wasp-lang.dev/blog/tags/careerweekly0.5https://wasp-lang.dev/blog/tags/chakraweekly0.5https://wasp-lang.dev/blog/tags/chatgptweekly0.5https://wasp-lang.dev/blog/tags/clean-codeweekly0.5https://wasp-lang.dev/blog/tags/cssweekly0.5https://wasp-lang.dev/blog/tags/databaseweekly0.5https://wasp-lang.dev/blog/tags/discordweekly0.5https://wasp-lang.dev/blog/tags/expressweekly0.5https://wasp-lang.dev/blog/tags/featureweekly0.5https://wasp-lang.dev/blog/tags/frameworkweekly0.5https://wasp-lang.dev/blog/tags/full-stackweekly0.5https://wasp-lang.dev/blog/tags/fullstackweekly0.5https://wasp-lang.dev/blog/tags/function-callingweekly0.5https://wasp-lang.dev/blog/tags/generateweekly0.5https://wasp-lang.dev/blog/tags/githubweekly0.5https://wasp-lang.dev/blog/tags/gitpodweekly0.5https://wasp-lang.dev/blog/tags/gptweekly0.5https://wasp-lang.dev/blog/tags/hackweekly0.5https://wasp-lang.dev/blog/tags/hackathonweekly0.5https://wasp-lang.dev/blog/tags/hacktoberfestweekly0.5https://wasp-lang.dev/blog/tags/haskellweekly0.5https://wasp-lang.dev/blog/tags/hiringweekly0.5https://wasp-lang.dev/blog/tags/indie-hackerweekly0.5https://wasp-lang.dev/blog/tags/interviewweekly0.5https://wasp-lang.dev/blog/tags/javascriptweekly0.5https://wasp-lang.dev/blog/tags/jobweekly0.5https://wasp-lang.dev/blog/tags/jobsweekly0.5https://wasp-lang.dev/blog/tags/junior-developersweekly0.5https://wasp-lang.dev/blog/tags/langchainweekly0.5https://wasp-lang.dev/blog/tags/languageweekly0.5https://wasp-lang.dev/blog/tags/laravelweekly0.5https://wasp-lang.dev/blog/tags/launch-weekweekly0.5https://wasp-lang.dev/blog/tags/mageweekly0.5https://wasp-lang.dev/blog/tags/marketingweekly0.5https://wasp-lang.dev/blog/tags/memeweekly0.5https://wasp-lang.dev/blog/tags/mlweekly0.5https://wasp-lang.dev/blog/tags/new-hireweekly0.5https://wasp-lang.dev/blog/tags/nextjsweekly0.5https://wasp-lang.dev/blog/tags/nodeweekly0.5https://wasp-lang.dev/blog/tags/nodejsweekly0.5https://wasp-lang.dev/blog/tags/open-sourceweekly0.5https://wasp-lang.dev/blog/tags/openaiweekly0.5https://wasp-lang.dev/blog/tags/optimisticweekly0.5https://wasp-lang.dev/blog/tags/pernweekly0.5https://wasp-lang.dev/blog/tags/prdweekly0.5https://wasp-lang.dev/blog/tags/prismaweekly0.5https://wasp-lang.dev/blog/tags/product-requirementweekly0.5https://wasp-lang.dev/blog/tags/product-updateweekly0.5https://wasp-lang.dev/blog/tags/programmingweekly0.5https://wasp-lang.dev/blog/tags/railsweekly0.5https://wasp-lang.dev/blog/tags/reactweekly0.5https://wasp-lang.dev/blog/tags/real-timeweekly0.5https://wasp-lang.dev/blog/tags/redditweekly0.5https://wasp-lang.dev/blog/tags/saa-sweekly0.5https://wasp-lang.dev/blog/tags/saasweekly0.5https://wasp-lang.dev/blog/tags/showcaseweekly0.5https://wasp-lang.dev/blog/tags/solopreneurweekly0.5https://wasp-lang.dev/blog/tags/startupweekly0.5https://wasp-lang.dev/blog/tags/startupsweekly0.5https://wasp-lang.dev/blog/tags/state-of-jsweekly0.5https://wasp-lang.dev/blog/tags/stripeweekly0.5https://wasp-lang.dev/blog/tags/supabaseweekly0.5https://wasp-lang.dev/blog/tags/techweekly0.5https://wasp-lang.dev/blog/tags/tech-careerweekly0.5https://wasp-lang.dev/blog/tags/tutorialweekly0.5https://wasp-lang.dev/blog/tags/typescriptweekly0.5https://wasp-lang.dev/blog/tags/updateweekly0.5https://wasp-lang.dev/blog/tags/updatesweekly0.5https://wasp-lang.dev/blog/tags/waspweekly0.5https://wasp-lang.dev/blog/tags/wasp-aiweekly0.5https://wasp-lang.dev/blog/tags/web-devweekly0.5https://wasp-lang.dev/blog/tags/web-developmentweekly0.5https://wasp-lang.dev/blog/tags/webdevweekly0.5https://wasp-lang.dev/blog/tags/websocketsweekly0.5https://wasp-lang.dev/blog/tags/windowsweekly0.5https://wasp-lang.dev/blog/tags/wslweekly0.5https://wasp-lang.dev/searchweekly0.5https://wasp-lang.dev/docs/0.11.8weekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.11.8/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/emailweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/uiweekly0.5https://wasp-lang.dev/docs/0.11.8/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.11.8/contactweekly0.5https://wasp-lang.dev/docs/0.11.8/contributingweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.11.8/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.11.8/editor-setupweekly0.5https://wasp-lang.dev/docs/0.11.8/general/cliweekly0.5https://wasp-lang.dev/docs/0.11.8/general/languageweekly0.5https://wasp-lang.dev/docs/0.11.8/project/client-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.11.8/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.11.8/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.11.8/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.11.8/project/server-configweekly0.5https://wasp-lang.dev/docs/0.11.8/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.11.8/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.11.8/project/testingweekly0.5https://wasp-lang.dev/docs/0.11.8/quick-startweekly0.5https://wasp-lang.dev/docs/0.11.8/telemetryweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.11.8/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.11.8/visionweekly0.5https://wasp-lang.dev/docs/0.11.8/writingguideweekly0.5https://wasp-lang.dev/docs/0.12.0weekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.12.0/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/emailweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/uiweekly0.5https://wasp-lang.dev/docs/0.12.0/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.12.0/contactweekly0.5https://wasp-lang.dev/docs/0.12.0/contributingweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.12.0/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.12.0/editor-setupweekly0.5https://wasp-lang.dev/docs/0.12.0/general/cliweekly0.5https://wasp-lang.dev/docs/0.12.0/general/languageweekly0.5https://wasp-lang.dev/docs/0.12.0/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/0.12.0/project/client-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.12.0/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.12.0/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.12.0/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.12.0/project/server-configweekly0.5https://wasp-lang.dev/docs/0.12.0/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.12.0/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.12.0/project/testingweekly0.5https://wasp-lang.dev/docs/0.12.0/quick-startweekly0.5https://wasp-lang.dev/docs/0.12.0/telemetryweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.12.0/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.12.0/visionweekly0.5https://wasp-lang.dev/docs/0.12.0/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/0.12.0/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/0.12.0/writingguideweekly0.5https://wasp-lang.dev/docs/0.13.0weekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/accessing-app-configweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/apisweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/emailweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/jobsweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/linksweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/0.13.0/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/emailweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/keycloakweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/uiweekly0.5https://wasp-lang.dev/docs/0.13.0/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/0.13.0/contactweekly0.5https://wasp-lang.dev/docs/0.13.0/contributingweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/backendsweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/crudweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/0.13.0/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/0.13.0/editor-setupweekly0.5https://wasp-lang.dev/docs/0.13.0/general/cliweekly0.5https://wasp-lang.dev/docs/0.13.0/general/languageweekly0.5https://wasp-lang.dev/docs/0.13.0/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/0.13.0/migrate-from-0-12-to-0-13weekly0.5https://wasp-lang.dev/docs/0.13.0/project/client-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/0.13.0/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/customizing-appweekly0.5https://wasp-lang.dev/docs/0.13.0/project/dependenciesweekly0.5https://wasp-lang.dev/docs/0.13.0/project/env-varsweekly0.5https://wasp-lang.dev/docs/0.13.0/project/server-configweekly0.5https://wasp-lang.dev/docs/0.13.0/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/0.13.0/project/static-assetsweekly0.5https://wasp-lang.dev/docs/0.13.0/project/testingweekly0.5https://wasp-lang.dev/docs/0.13.0/quick-startweekly0.5https://wasp-lang.dev/docs/0.13.0/telemetryweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/authweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/createweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/0.13.0/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/0.13.0/visionweekly0.5https://wasp-lang.dev/docs/0.13.0/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/0.13.0/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/0.13.0/writingguideweekly0.5https://wasp-lang.dev/docsweekly0.5https://wasp-lang.dev/docs/advanced/accessing-app-configweekly0.5https://wasp-lang.dev/docs/advanced/apisweekly0.5https://wasp-lang.dev/docs/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/advanced/emailweekly0.5https://wasp-lang.dev/docs/advanced/jobsweekly0.5https://wasp-lang.dev/docs/advanced/linksweekly0.5https://wasp-lang.dev/docs/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/auth/auth-hooksweekly0.5https://wasp-lang.dev/docs/auth/emailweekly0.5https://wasp-lang.dev/docs/auth/entitiesweekly0.5https://wasp-lang.dev/docs/auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/social-auth/discordweekly0.5https://wasp-lang.dev/docs/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/auth/social-auth/keycloakweekly0.5https://wasp-lang.dev/docs/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/uiweekly0.5https://wasp-lang.dev/docs/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/contactweekly0.5https://wasp-lang.dev/docs/contributingweekly0.5https://wasp-lang.dev/docs/data-model/backendsweekly0.5https://wasp-lang.dev/docs/data-model/crudweekly0.5https://wasp-lang.dev/docs/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/data-model/prisma-fileweekly0.5https://wasp-lang.dev/docs/editor-setupweekly0.5https://wasp-lang.dev/docs/general/cliweekly0.5https://wasp-lang.dev/docs/general/languageweekly0.5https://wasp-lang.dev/docs/general/typescriptweekly0.5https://wasp-lang.dev/docs/migrate-from-0-11-to-0-12weekly0.5https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13weekly0.5https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14weekly0.5https://wasp-lang.dev/docs/project/client-configweekly0.5https://wasp-lang.dev/docs/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/project/customizing-appweekly0.5https://wasp-lang.dev/docs/project/dependenciesweekly0.5https://wasp-lang.dev/docs/project/env-varsweekly0.5https://wasp-lang.dev/docs/project/server-configweekly0.5https://wasp-lang.dev/docs/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/project/static-assetsweekly0.5https://wasp-lang.dev/docs/project/testingweekly0.5https://wasp-lang.dev/docs/quick-startweekly0.5https://wasp-lang.dev/docs/telemetryweekly0.5https://wasp-lang.dev/docs/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/tutorial/authweekly0.5https://wasp-lang.dev/docs/tutorial/createweekly0.5https://wasp-lang.dev/docs/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/visionweekly0.5https://wasp-lang.dev/docs/wasp-ai/creating-new-appweekly0.5https://wasp-lang.dev/docs/wasp-ai/developing-existing-appweekly0.5https://wasp-lang.dev/docs/writingguideweekly0.5https://wasp-lang.dev/weekly0.5 \ No newline at end of file