├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── apps ├── pocketbase │ ├── .dockerignore │ ├── Dockerfile │ ├── fly.toml │ ├── package.json │ ├── pb_data │ │ └── .gitignore │ └── pb_migrations │ │ ├── 1722064611_updated_users.js │ │ ├── 1722310396_created_posts.js │ │ ├── 1722316694_updated_users.js │ │ └── README.md └── web │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── index.test.ts │ ├── lib │ │ ├── components │ │ │ ├── MenuListItems.svelte │ │ │ ├── Post.svelte │ │ │ └── SignOutButton.svelte │ │ ├── db │ │ │ ├── create-post.js │ │ │ ├── delete-post.js │ │ │ ├── get-posts.js │ │ │ └── update-post.js │ │ ├── icons │ │ │ ├── Check.svelte │ │ │ └── Trash.svelte │ │ ├── index.ts │ │ ├── pocketbase.js │ │ └── server │ │ │ └── pocketbase.js │ ├── routes │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── api │ │ │ ├── auth-token-required │ │ │ │ └── +server.js │ │ │ └── create-account │ │ │ │ └── +server.js │ │ ├── create-account │ │ │ └── +page.svelte │ │ ├── home │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ └── crud-example │ │ │ │ └── +page.svelte │ │ ├── reset-password │ │ │ └── +page.svelte │ │ └── sign-in │ │ │ └── +page.svelte │ └── types │ │ ├── api.js │ │ ├── app.js │ │ └── db.js │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tailwind.config.js │ ├── tests │ └── test.ts │ ├── tsconfig.json │ └── vite.config.ts ├── docker-compose.yml ├── package-lock.json ├── package.json ├── packages └── config-eslint │ ├── index.js │ └── package.json └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # svelte 12 | .svelte-kit 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | 18 | # debug 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # local env files 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | # turbo 30 | .turbo 31 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .svelte-kit 3 | node_modules 4 | /build 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js* 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | pnpm-workspace.yaml 14 | package-lock.json 15 | yarn.lock 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 100, 5 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 6 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 arrowban 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SvelteKit, Pocketbase, Turborepo Template 2 | 3 | A template for an extensible SvelteKit + Pocketbase + Turborepo codebase. 4 | 5 | Comes with... 6 | 7 | - **TailwindCSS and DaisyUI:** [Customize the theme](https://daisyui.com/docs/themes) and use [semantic components class names](https://daisyui.com/components) 8 | - **Pre-built account creation, sign in, and reset password flows** 9 | - **Basic CRUD example** 10 | - **Deployment configuration for Pocketbase on [Fly.io](https://fly.io)** 11 | 12 | See the live demo [here](https://sveltekit-pocketbase-turborepo-template.pages.dev), deployed directly from the `demo` branch of this repository. 13 | 14 | _Note: The SvelteKit app supports TypeScript with no additional configuration._ 15 | 16 | ### Sections 17 | 18 | - [Screenshots](#screenshots) 19 | - [Requirements](#requirements) 20 | - [Getting started](#getting-started) 21 | - [Set up the development environment](#set-up-the-development-environment) 22 | - [Production deployments](#production-deployments) 23 | - [Deploy the backend](#deploy-the-backend) 24 | - [Deploy on Fly.io](#deploy-on-flyio) 25 | - [Deploy the web app](#deploy-the-web-app) 26 | - [The code](#the-code) 27 | - [Apps and Packages](#apps-and-packages) 28 | - [Utilities](#utilities) 29 | 30 | ### Screenshots 31 | 32 | ![landing-preview-light](https://github.com/user-attachments/assets/497ca0f4-90ed-422c-8997-7fbd3b85f0a6) 33 | ![landing-preview-dark](https://github.com/user-attachments/assets/d9995fdf-7c81-4679-bf81-aef7dfd24401) 34 | ![sign-in-preview-light](https://github.com/user-attachments/assets/0a183d67-9f0e-404c-9ad9-69d3edf94466) 35 | ![sign-in-preview-dark](https://github.com/user-attachments/assets/f08f06ee-4701-4019-bfe8-50fe7db223f6) 36 | ![home-preview-light](https://github.com/user-attachments/assets/f4fc8e66-c5b7-4431-bc68-5d38b2120914) 37 | ![home-preview-dark](https://github.com/user-attachments/assets/7fc86aa1-273a-49c6-8794-b1d178af0324) 38 | ![crud-preview-light](https://github.com/user-attachments/assets/e26d22f9-4b8b-41ea-93ae-0f005fa7d6bd) 39 | ![crud-preview-dark](https://github.com/user-attachments/assets/54a0e7a5-b13c-4bfa-b2f8-6e84d551a4fb) 40 | 41 | ## Requirements 42 | 43 | [Node.js](https://nodejs.org), and [Docker Desktop](https://www.docker.com/products/docker-desktop) ([Docker](https://www.docker.com) + [Docker Compose](https://docs.docker.com/compose)). Make sure docker is running before getting started. 44 | 45 | ## Getting started 46 | 47 | Create a new repository with the "use this template" button on GitHub, or run the following commands: 48 | 49 | ```sh 50 | git clone --depth=1 --branch=main https://github.com/arrowban/sveltekit-pocketbase-turborepo-template.git my-app 51 | 52 | cd my-app 53 | rm -rf .git 54 | 55 | git init 56 | git add -A 57 | git commit -m "Initial commit" 58 | ``` 59 | 60 | ### Set up the development environment 61 | 62 | 1. Install dependencies: 63 | 64 | ```sh 65 | npm install 66 | ``` 67 | 68 | 2. Set up environment variables: 69 | 70 | ```sh 71 | cp apps/web/.env.example apps/web/.env.local 72 | ``` 73 | 74 | 3. Start the development server and backend 75 | 76 | ```sh 77 | npm run dev 78 | ``` 79 | 80 | 4. Go to [the local Pocketbase admin settings page](http://localhost:8090/_) to create an admin account for the local backend 81 | 5. Update environment variables in `apps/web/.env.local` with your admin credentials: 82 | 83 | ``` 84 | POCKETBASE_ADMIN_EMAIL=youremail@example.com 85 | POCKETBASE_ADMIN_PASSWORD=your-strong-password 86 | PUBLIC_POCKETBASE_BASE_URL=http://localhost:8090 87 | ``` 88 | 89 | 6. [Create an account](http://localhost:5173/create-account) on the local web app, and [start building](http://localhost:5173/home) 90 | 91 | ## Production deployments 92 | 93 | Things to consider for production deployments: 94 | 95 | 1. [Add SMTP server settings](https://pocketbase.io/docs/going-to-production/#use-smtp-mail-server) for sending verification and reset password emails 96 | - Consider turning on the "Forbid authentication for unverified users" option for the users table 97 | - Remember to edit the email templates from the Pocketbase admin settings after deploying 98 | 2. [Integrate OAuth2 providers](https://pocketbase.io/docs/authentication/#oauth2-integration) 99 | 100 | ### Deploy the backend 101 | 102 | It is straightforward to [host Pocketbase](https://pocketbase.io/docs/going-to-production/#deployment-strategies) on any VPS. This template comes configured for easy deployment of Pocketbase on [Fly.io](https://fly.io). 103 | 104 | #### Deploy on Fly.io 105 | 106 | 1. [Install flyctl](https://fly.io/docs/flyctl/install) – the open-source Fly.io CLI 107 | 2. Create an account with `fly auth signup` or log in with `fly auth login` 108 | 3. Create a new app 109 | 110 | ```sh 111 | fly apps create --generate-name 112 | ``` 113 | 114 | 4. Add the generated app name to `apps/pocketbase/fly.toml` (line 1) 115 | 5. [Choose the region](https://fly.io/docs/reference/regions) closest to you (or your users) and add the corresponding region ID as the `primary_region` in `apps/pocketbase/fly.toml` (line 2) 116 | 6. Create a new volume, using the same region as the one you chose in step 5 (size can easily be [extended anytime](https://fly.io/docs/volumes/volume-manage/#extend-a-volume)) 117 | 118 | ```sh 119 | fly volumes create pb_data --size=1 # Creates a volume with 1GB of storage 120 | ``` 121 | 122 | 7. Deploy the Pocketbase server, and run this command again anytime you want to update the deployment after making changes locally 123 | 124 | ```sh 125 | npm run deploy 126 | ``` 127 | 128 | 8. Go to the production Pocketbase admin settings page at `https://APP_NAME.fly.dev/_` to create an admin account for the production backend 129 | 9. Add production environment variables to a new file `apps/web/.env.production` with your production admin credentials: 130 | 131 | ``` 132 | POCKETBASE_ADMIN_EMAIL=youremail@example.com 133 | POCKETBASE_ADMIN_PASSWORD=your-strong-password 134 | PUBLIC_POCKETBASE_BASE_URL=https://APP_NAME.fly.dev 135 | ``` 136 | 137 | The pre-configured VM is the cheapest available on Fly.io (`shared-cpu-1x` with 256MB of memory), [scale the backend vertically](https://fly.io/docs/launch/scale-machine) as your app grows. 138 | 139 | ### Deploy the web app 140 | 141 | Many cloud platforms provide generous free tiers for deploying web apps built with popular frameworks like SvelteKit. Cloudflare, Vercel, and Netlify are just a few examples of platforms that integrate directly with GitHub repositories and provide seamless CI/CD. 142 | 143 | When setting up your first deployment on any of these platforms, remember to: 144 | 145 | - Replace `@sveltejs/adapter-auto` with the [appropriate adapter](https://kit.svelte.dev/docs/adapters) in `apps/web/svelte.config.js` 146 | - Specify `apps/web` as the root of the web app 147 | - Add environment variables from `apps/web/.env.production` to the deployment 148 | 149 | **I strongly recommend deploying the web app with [Cloudflare pages](https://pages.cloudflare.com).** 150 | 151 | ## The code 152 | 153 | ### Apps and Packages 154 | 155 | - `apps/pocketbase`: Dockerized [Pocketbase](https://pocketbase.io) backend 156 | - `apps/web`: [SvelteKit](https://kit.svelte.dev) app 157 | - `packages/eslint-config-custom`: `eslint` configurations 158 | 159 | ### Utilities 160 | 161 | This template has some additional tools already setup for you: 162 | 163 | - [TypeScript](https://www.typescriptlang.org) for static type checking 164 | - [ESLint](https://eslint.org) for code linting 165 | - [Prettier](https://prettier.io) for code formatting 166 | -------------------------------------------------------------------------------- /apps/pocketbase/.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from pb_data/.gitignore 2 | pb_data/**/* 3 | !pb_data/**/.gitignore 4 | fly.toml 5 | -------------------------------------------------------------------------------- /apps/pocketbase/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3 as downloader 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG TARGETVARIANT 6 | ARG VERSION 7 | 8 | ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}${TARGETVARIANT}" 9 | 10 | RUN wget https://github.com/pocketbase/pocketbase/releases/download/v${VERSION}/pocketbase_${VERSION}_${BUILDX_ARCH}.zip \ 11 | && unzip pocketbase_${VERSION}_${BUILDX_ARCH}.zip \ 12 | && chmod +x /pocketbase 13 | 14 | FROM alpine:3 15 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 16 | 17 | COPY ./pb_migrations /pb_migrations 18 | 19 | EXPOSE 8090 20 | 21 | COPY --from=downloader /pocketbase /usr/local/bin/pocketbase 22 | ENTRYPOINT ["/usr/local/bin/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data", "--migrationsDir=/pb_migrations", "--publicDir=/pb_public", "--hooksDir=/pb_hooks"] 23 | -------------------------------------------------------------------------------- /apps/pocketbase/fly.toml: -------------------------------------------------------------------------------- 1 | app = "" # Add app name generated by `fly apps create --generate-name` 2 | primary_region = "" # Add a region ID from https://fly.io/docs/reference/regions 3 | 4 | [mounts] 5 | destination = "/pb_data" 6 | source = "pb_data" # Create volume with `fly volumes create pb_data --size=1` 7 | 8 | [build.args] 9 | VERSION = "0.22.18" 10 | 11 | [http_service] 12 | internal_port = 8090 13 | force_https = true 14 | auto_stop_machines = "stop" 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ["app"] 18 | 19 | [checks] 20 | [checks.health] 21 | grace_period = "30s" 22 | interval = "15s" 23 | method = "get" 24 | path = "/api/health" 25 | port = 8090 26 | timeout = "10s" 27 | type = "http" 28 | 29 | [[vm]] 30 | size = "shared-cpu-1x" 31 | memory = 256 32 | -------------------------------------------------------------------------------- /apps/pocketbase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pb", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "docker compose up", 7 | "deploy": "fly deploy" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/pocketbase/pb_data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /apps/pocketbase/pb_migrations/1722064611_updated_users.js: -------------------------------------------------------------------------------- 1 | /// 2 | migrate( 3 | (db) => { 4 | const dao = new Dao(db); 5 | const collection = dao.findCollectionByNameOrId('_pb_users_auth_'); 6 | 7 | collection.createRule = null; 8 | 9 | return dao.saveCollection(collection); 10 | }, 11 | (db) => { 12 | const dao = new Dao(db); 13 | const collection = dao.findCollectionByNameOrId('_pb_users_auth_'); 14 | 15 | collection.createRule = ''; 16 | 17 | return dao.saveCollection(collection); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /apps/pocketbase/pb_migrations/1722310396_created_posts.js: -------------------------------------------------------------------------------- 1 | /// 2 | migrate( 3 | (db) => { 4 | const collection = new Collection({ 5 | id: 'eqee25l2eod5yc9', 6 | created: '2024-07-30 03:33:16.509Z', 7 | updated: '2024-07-30 03:33:16.509Z', 8 | name: 'posts', 9 | type: 'base', 10 | system: false, 11 | schema: [ 12 | { 13 | system: false, 14 | id: 'typah2jc', 15 | name: 'user', 16 | type: 'relation', 17 | required: true, 18 | presentable: false, 19 | unique: false, 20 | options: { 21 | collectionId: '_pb_users_auth_', 22 | cascadeDelete: false, 23 | minSelect: null, 24 | maxSelect: 1, 25 | displayFields: null 26 | } 27 | }, 28 | { 29 | system: false, 30 | id: '43edmncd', 31 | name: 'count', 32 | type: 'number', 33 | required: false, 34 | presentable: false, 35 | unique: false, 36 | options: { 37 | min: null, 38 | max: null, 39 | noDecimal: false 40 | } 41 | } 42 | ], 43 | indexes: [], 44 | listRule: '@request.auth.id != ""', 45 | viewRule: '@request.auth.id != ""', 46 | createRule: 'user = @request.auth.id', 47 | updateRule: 'user = @request.auth.id', 48 | deleteRule: 'user = @request.auth.id', 49 | options: {} 50 | }); 51 | 52 | return Dao(db).saveCollection(collection); 53 | }, 54 | (db) => { 55 | const dao = new Dao(db); 56 | const collection = dao.findCollectionByNameOrId('eqee25l2eod5yc9'); 57 | 58 | return dao.deleteCollection(collection); 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /apps/pocketbase/pb_migrations/1722316694_updated_users.js: -------------------------------------------------------------------------------- 1 | /// 2 | migrate( 3 | (db) => { 4 | const dao = new Dao(db); 5 | const collection = dao.findCollectionByNameOrId('_pb_users_auth_'); 6 | 7 | collection.viewRule = '@request.auth.id != ""'; 8 | 9 | return dao.saveCollection(collection); 10 | }, 11 | (db) => { 12 | const dao = new Dao(db); 13 | const collection = dao.findCollectionByNameOrId('_pb_users_auth_'); 14 | 15 | collection.viewRule = 'id = @request.auth.id'; 16 | 17 | return dao.saveCollection(collection); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /apps/pocketbase/pb_migrations/README.md: -------------------------------------------------------------------------------- 1 | This directory stores database migrations. 2 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | POCKETBASE_ADMIN_EMAIL=email@example.com 2 | POCKETBASE_ADMIN_PASSWORD=example-password 3 | PUBLIC_POCKETBASE_BASE_URL=http://localhost:8090 4 | -------------------------------------------------------------------------------- /apps/web/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@repo/eslint-config/index.js'] 3 | }; 4 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /apps/web/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | vite.config.*.timestamp-* 15 | -------------------------------------------------------------------------------- /apps/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test": "npm run test:integration && npm run test:unit", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "lint": "eslint .", 14 | "test:integration": "playwright test", 15 | "test:unit": "vitest" 16 | }, 17 | "dependencies": { 18 | "pocketbase": "^0.21.3" 19 | }, 20 | "devDependencies": { 21 | "@playwright/test": "^1.42.1", 22 | "@repo/eslint-config": "*", 23 | "@sveltejs/adapter-auto": "^3.1.1", 24 | "@sveltejs/kit": "^2.5.2", 25 | "@sveltejs/vite-plugin-svelte": "^3.0.2", 26 | "@typescript-eslint/eslint-plugin": "^7.1.0", 27 | "@typescript-eslint/parser": "^7.1.0", 28 | "autoprefixer": "^10.4.19", 29 | "daisyui": "^4.12.10", 30 | "eslint": "^8.57.0", 31 | "postcss": "^8.4.40", 32 | "prettier": "^3.2.5", 33 | "prettier-plugin-svelte": "^3.2.2", 34 | "prettier-plugin-tailwindcss": "^0.6.5", 35 | "svelte": "^4.2.12", 36 | "svelte-check": "^3.6.6", 37 | "tailwindcss": "^3.4.7", 38 | "tslib": "^2.6.2", 39 | "typescript": "^5.3.3", 40 | "vite": "^5.1.4", 41 | "vitest": "^1.3.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/ 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /apps/web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /apps/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/MenuListItems.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
  • CRUD example
  • 6 |
  • 7 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/Post.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
    31 |
    32 |

    Post ID: {post.id}

    33 |

    User ID: {post.user}

    34 | {#if post.expand} 35 |

    Username: {post.expand.user.username}

    36 | {/if} 37 |

    Count: {post.count}

    38 |
    39 | 46 | 49 |
    50 |
    51 |

    Created: {post.created}

    52 |

    Updated: {post.updated}

    53 |
    54 |
    55 |
    56 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/SignOutButton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/lib/db/create-post.js: -------------------------------------------------------------------------------- 1 | import { pb } from '$lib/pocketbase'; 2 | 3 | /** 4 | * @param {Pick} data 5 | */ 6 | export async function createPost(data) { 7 | try { 8 | /** @type {PostRecordModel} */ 9 | const record = await pb.collection('posts').create(data, { expand: 'user' }); 10 | return { data: record }; 11 | } catch (err) { 12 | return { error: /** @type {Error} */ (err).message }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/db/delete-post.js: -------------------------------------------------------------------------------- 1 | import { pb } from '$lib/pocketbase'; 2 | 3 | /** 4 | * @param {string} id 5 | */ 6 | export async function deletePost(id) { 7 | try { 8 | await pb.collection('posts').delete(id); 9 | return {}; 10 | } catch (err) { 11 | return { error: /** @type {Error} */ (err).message }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/lib/db/get-posts.js: -------------------------------------------------------------------------------- 1 | import { pb } from '$lib/pocketbase'; 2 | 3 | export async function getPosts() { 4 | try { 5 | /** @type {PostRecordModel[]} */ 6 | const fullList = await pb.collection('posts').getFullList({ expand: 'user', sort: '-created' }); 7 | return { data: fullList }; 8 | } catch (err) { 9 | return { error: /** @type {Error} */ (err).message }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/lib/db/update-post.js: -------------------------------------------------------------------------------- 1 | import { pb } from '$lib/pocketbase'; 2 | 3 | /** 4 | * @param {string} id 5 | * @param {Partial>} data 6 | */ 7 | export async function updatePost(id, data) { 8 | try { 9 | /** @type {PostRecordModel} */ 10 | const record = await pb.collection('posts').update(id, data, { expand: 'user' }); 11 | return { data: record }; 12 | } catch (err) { 13 | return { error: /** @type {Error} */ (err).message }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/Check.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/Trash.svelte: -------------------------------------------------------------------------------- 1 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /apps/web/src/lib/pocketbase.js: -------------------------------------------------------------------------------- 1 | import { PUBLIC_POCKETBASE_BASE_URL } from '$env/static/public'; 2 | import PocketBase from 'pocketbase'; 3 | 4 | export const pb = new PocketBase(PUBLIC_POCKETBASE_BASE_URL); 5 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/pocketbase.js: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private'; 2 | import { PUBLIC_POCKETBASE_BASE_URL } from '$env/static/public'; 3 | import PocketBase from 'pocketbase'; 4 | 5 | export function newPocketBase() { 6 | const baseUrl = env.POCKETBASE_BASE_URL || PUBLIC_POCKETBASE_BASE_URL; 7 | return new PocketBase(baseUrl); 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /apps/web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 |
    16 |
    17 |

    SvelteKit, Pocketbase, Turborepo Template

    18 |

    Don't forget to name this thing.

    19 | 20 |
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /apps/web/src/routes/api/auth-token-required/+server.js: -------------------------------------------------------------------------------- 1 | import { newPocketBase } from '$lib/server/pocketbase'; 2 | import { json } from '@sveltejs/kit'; 3 | 4 | /** @type {import('./$types').RequestHandler} */ 5 | export async function POST({ request }) { 6 | try { 7 | const authHeader = request.headers.get('x-auth-token'); 8 | if (!authHeader) { 9 | throw new Error('Unauthorized.'); 10 | } 11 | 12 | const body = await request.json(); 13 | 14 | const pb = newPocketBase(); 15 | pb.authStore.save(authHeader); 16 | 17 | /** @type {UserAuthRefresh} */ 18 | const user = await pb.collection('users').authRefresh(); 19 | 20 | // Do stuff... 21 | 22 | pb.authStore.clear(); 23 | 24 | return json({ 25 | data: { body, user } 26 | }); 27 | } catch (err) { 28 | return json({ 29 | error: /** @type {Error} */ (err).message 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/src/routes/api/create-account/+server.js: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private'; 2 | import { newPocketBase } from '$lib/server/pocketbase'; 3 | import { json } from '@sveltejs/kit'; 4 | 5 | /** @type {import('./$types').RequestHandler} */ 6 | export async function POST({ request }) { 7 | try { 8 | /** @type {ApiCreateAccountParams} */ 9 | const body = await request.json(); 10 | const { email, password, passwordConfirm } = body; 11 | 12 | const pb = newPocketBase(); 13 | await pb.admins.authWithPassword(env.POCKETBASE_ADMIN_EMAIL, env.POCKETBASE_ADMIN_PASSWORD); 14 | const createUserRecordModel = await pb 15 | .collection('users') 16 | .create({ email, password, passwordConfirm }); 17 | pb.authStore.clear(); 18 | 19 | return json({ 20 | data: { createUserRecordModel } 21 | }); 22 | } catch (err) { 23 | return json({ 24 | error: /** @type {Error} */ (err).message 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/routes/create-account/+page.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | SvelteKit, Pocketbase, Turborepo Template 52 |
    53 |
    56 |
    57 |
    58 |

    Create your account

    59 |
    60 | 63 | 71 |
    72 |
    73 | 76 | 84 |
    85 |
    86 | 89 | 97 |
    98 |
    99 | 106 | {#if error} 107 |

    {error}

    108 | {/if} 109 |
    110 |
    111 |
    112 |
    113 |

    Already have an account?

    114 | Click here to sign in. 115 |
    116 |
    117 |
    118 | -------------------------------------------------------------------------------- /apps/web/src/routes/home/+layout.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#if $user} 22 |
    23 | 24 |
    25 | 26 | 52 | 53 | 54 |
    55 |
    56 | 57 | 61 |
    62 |
    63 | {/if} 64 | -------------------------------------------------------------------------------- /apps/web/src/routes/home/+page.svelte: -------------------------------------------------------------------------------- 1 |
    Your next amazing project 🎉
    2 | -------------------------------------------------------------------------------- /apps/web/src/routes/home/crud-example/+page.svelte: -------------------------------------------------------------------------------- 1 | 108 | 109 |
    110 |

    CRUD example

    111 | {#if error} 112 |

    {error}

    113 | {/if} 114 | 115 | {#if data === null} 116 |
    117 | {:else} 118 |
      119 | {#each data as post, index} 120 |
    • 121 | onClickDeletePost(index)} 124 | incrementCount={() => onClickIncrementCount(index)} 125 | /> 126 |
    • 127 | {/each} 128 |
    129 | {/if} 130 |
    131 | -------------------------------------------------------------------------------- /apps/web/src/routes/reset-password/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | SvelteKit, Pocketbase, Turborepo Template 33 |
    34 |
    37 |
    38 |
    39 |

    40 | {#if success}Reset password email sent!{:else}Reset your password{/if} 41 |

    42 | {#if !success} 43 |
    44 | 47 | 55 |
    56 | {/if} 57 |
    58 | 67 | {#if error} 68 |

    {error}

    69 | {/if} 70 |
    71 |
    72 |
    73 |
    74 |

    Remember your password?

    75 | Click here to sign in. 76 |
    77 |
    78 |
    79 | -------------------------------------------------------------------------------- /apps/web/src/routes/sign-in/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | SvelteKit, Pocketbase, Turborepo Template 31 |
    32 |
    33 |
    34 |
    35 |

    Sign in to your account

    36 |
    37 | 40 | 48 |
    49 |
    50 | 53 | 61 | 66 |
    67 |
    68 | 75 | {#if error} 76 |

    {error}

    77 | {/if} 78 |
    79 |
    80 |
    81 |
    82 |

    Don't have an account yet?

    83 | Click here to create one. 84 |
    85 |
    86 |
    87 | -------------------------------------------------------------------------------- /apps/web/src/types/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {object} ApiCreateAccountParams 3 | * @property {string} email 4 | * @property {string} password 5 | * @property {string} passwordConfirm 6 | */ 7 | -------------------------------------------------------------------------------- /apps/web/src/types/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("svelte/store").Writable} TokenStore 3 | * @typedef {import("svelte/store").Writable} UserStore 4 | */ 5 | -------------------------------------------------------------------------------- /apps/web/src/types/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("pocketbase").RecordModel } RecordModel 3 | * 4 | * @typedef {object} User 5 | * @property {string} id 6 | * @property {string} created 7 | * @property {string} updated 8 | * @property {string} username 9 | * @property {string=} email 10 | * @property {boolean} emailVisibility 11 | * @property {boolean} verified 12 | * @property {string} name 13 | * @property {string} avatar 14 | * 15 | * @typedef {RecordModel & User } UserRecordModel 16 | * @typedef {import("pocketbase").RecordAuthResponse } UserAuthRefresh 17 | * 18 | * @typedef {object} Post 19 | * @property {string} id 20 | * @property {string} created 21 | * @property {string} updated 22 | * @property {string} user 23 | * @property {number} count 24 | * @property {{user: User}=} expand 25 | * 26 | * @typedef {RecordModel & Post} PostRecordModel 27 | */ 28 | -------------------------------------------------------------------------------- /apps/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arrowban/sveltekit-pocketbase-turborepo-template/0bda7f92418cb2a209867f5c80836c2426013491/apps/web/static/favicon.png -------------------------------------------------------------------------------- /apps/web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | adapter: adapter() 9 | } 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [require('daisyui')] 8 | }; 9 | -------------------------------------------------------------------------------- /apps/web/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect(page.getByRole('heading', { name: 'Web' })).toBeVisible(); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'] 8 | }, 9 | build: { 10 | commonjsOptions: { 11 | include: [/@repo\/ui/, /node_modules/] 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pocketbase: 3 | build: 4 | context: ./apps/pocketbase 5 | args: 6 | - VERSION=0.22.18 7 | restart: unless-stopped 8 | ports: 9 | - '8090:8090' 10 | volumes: 11 | - ./apps/pocketbase/pb_data:/pb_data 12 | - ./apps/pocketbase/pb_migrations:/pb_migrations 13 | # - /path/to/public:/pb_public 14 | # - /path/to/hooks:/pb_hooks 15 | healthcheck: 16 | test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1 17 | interval: 5s 18 | timeout: 5s 19 | retries: 5 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev", 6 | "check": "turbo run check", 7 | "lint": "turbo run lint", 8 | "format": "prettier --write .", 9 | "deploy": "turbo run deploy" 10 | }, 11 | "devDependencies": { 12 | "prettier": "^3.2.5", 13 | "prettier-plugin-svelte": "^3.2.2", 14 | "turbo": "^2.0.9" 15 | }, 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "name": "sveltekit-pocketbase-turborepo-template", 20 | "packageManager": "npm@10.2.5", 21 | "workspaces": [ 22 | "apps/*", 23 | "packages/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/config-eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier', 8 | 'turbo' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'] 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser' 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /packages/config-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@typescript-eslint/eslint-plugin": "^7.1.0", 6 | "@typescript-eslint/parser": "^7.1.0", 7 | "eslint-config-prettier": "^9.1.0", 8 | "eslint-config-turbo": "^2.0.0", 9 | "eslint-plugin-svelte": "^2.35.1" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "inputs": ["$TURBO_DEFAULT", ".env*"], 7 | "outputs": [".svelte-kit/**", ".vercel/**"] 8 | }, 9 | "check": {}, 10 | "lint": {}, 11 | "dev": { 12 | "cache": false, 13 | "persistent": true 14 | }, 15 | "deploy": { 16 | "cache": false 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------