16 Commits

Author SHA1 Message Date
ca421b313d bump: v0.0.24 -> v0.0.25 2025-05-14 04:38:55 -04:00
71df7b4711 fix: assign git root 2025-05-14 04:32:02 -04:00
106a43aaf1 style: rename release 2025-05-14 04:30:28 -04:00
5077682fa5 style: break out check from release 2025-05-14 04:28:27 -04:00
634bff4411 feat: readme 2025-05-14 04:15:58 -04:00
bce4d598fb build(nix): updated nix hashes 2025-05-14 01:45:17 -04:00
3fd1e1f4a3 build(client): updated npm dependencies 2025-05-14 01:43:45 -04:00
de1baa4517 build(nix): updated nix hashes 2025-05-14 00:11:08 -04:00
be309409ad build(client): updated npm dependencies 2025-05-14 00:09:42 -04:00
b6ef0cab53 license 2025-05-13 21:31:58 -04:00
e8d9a4adff feat: bump openapi version 2025-05-13 18:19:47 -04:00
fc90905dcf bump: v0.0.23 -> v0.0.24 2025-05-13 18:12:26 -04:00
6494d74ab2 style: rename workflows 2025-05-13 18:09:44 -04:00
ca313960c4 style: rename openapi.yaml 2025-05-13 18:01:54 -04:00
0cb262524c fix: format sql and proto 2025-05-13 17:59:22 -04:00
05aff14703 feat: generate release notes 2025-05-13 17:41:38 -04:00
15 changed files with 204 additions and 70 deletions

View File

@ -1,14 +1,19 @@
name: Lint Workflow name: Check
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, reopened, edited]
jobs: jobs:
lint: check:
name: check
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
contains(github.event.head_commit.message, 'bump:') == false &&
contains(github.event.head_commit.message, 'Merge pull request') == false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -22,7 +27,7 @@ jobs:
uses: cachix/cachix-action@v16 uses: cachix/cachix-action@v16
with: with:
name: trevstack name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Run checks - name: Check
run: nix flake check run: nix flake check

View File

@ -1,16 +1,16 @@
name: Release Workflow name: Release
on: on:
push: push:
tags: tags:
- '*' - "*"
permissions: permissions:
contents: write contents: write
packages: write packages: write
jobs: jobs:
release: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -25,7 +25,28 @@ jobs:
uses: cachix/cachix-action@v16 uses: cachix/cachix-action@v16
with: with:
name: trevstack name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Check
run: nix flake check
release:
runs-on: ubuntu-latest
needs: check
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Use Cachix
uses: cachix/cachix-action@v16
with:
name: trevstack
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build - name: Build
run: > run: >
@ -37,9 +58,10 @@ jobs:
.#trevstack-darwin-amd64 .#trevstack-darwin-amd64
.#trevstack-darwin-arm64 .#trevstack-darwin-arm64
- name: Create Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
generate_release_notes: true
files: |- files: |-
result*/bin/* result*/bin/*

View File

@ -1,4 +1,4 @@
name: Update Workflow name: Update
on: on:
schedule: schedule:
@ -21,7 +21,7 @@ jobs:
uses: cachix/cachix-action@v16 uses: cachix/cachix-action@v16
with: with:
name: trevstack name: trevstack
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# https://github.com/actions/checkout/issues/13 # https://github.com/actions/checkout/issues/13
- name: Set Git Config - name: Set Git Config
@ -37,5 +37,3 @@ jobs:
with: with:
delete-branch: true delete-branch: true
title: Bump deps title: Bump deps

View File

@ -3,7 +3,23 @@
git_root=$(git rev-parse --show-toplevel) git_root=$(git rev-parse --show-toplevel)
git_version=$(git describe --tags --abbrev=0) git_version=$(git describe --tags --abbrev=0)
version=${git_version#v} version=${git_version#v}
next_version=$(echo "${version}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
major=$(echo "${version}" | cut -d . -f1)
minor=$(echo "${version}" | cut -d . -f2)
patch=$(echo "${version}" | cut -d . -f3)
case "${1}" in
major) major=$((major + 1)) ;;
minor) minor=$((minor + 1)) ;;
*) patch=$((patch + 1)) ;;
esac
next_version="${major}.${minor}.${patch}"
echo "${version} -> ${next_version}"
echo "bumping openapi"
cd "${git_root}"
sed -i -e "s/${version}/${next_version}/g" openapi.yaml
git add openapi.yaml
echo "bumping client" echo "bumping client"
cd "${git_root}/client" cd "${git_root}/client"

View File

@ -2,6 +2,7 @@
"recommendations": [ "recommendations": [
"golang.go", "golang.go",
"dorzey.vscode-sqlfluff", "dorzey.vscode-sqlfluff",
"zxh404.vscode-proto3",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"svelte.svelte-vscode", "svelte.svelte-vscode",
"esbenp.prettier-vscode" "esbenp.prettier-vscode"

View File

@ -4,6 +4,7 @@
// Go // Go
"go.lintTool": "revive", "go.lintTool": "revive",
"go.formatTool": "goimports", "go.formatTool": "goimports",
"go.buildTags": "dev",
"go.lintFlags": ["--config=server/revive.toml"], "go.lintFlags": ["--config=server/revive.toml"],
"gopls": { "ui.semanticTokens": true }, "gopls": { "ui.semanticTokens": true },
"[go]": { "[go]": {
@ -12,6 +13,14 @@
// SQLFluff // SQLFluff
"sqlfluff.config": "server/db/.sqlfluff", "sqlfluff.config": "server/db/.sqlfluff",
"[sql]": {
"editor.defaultFormatter": "dorzey.vscode-sqlfluff"
},
// Proto
"[proto3]": {
"editor.defaultFormatter": "zxh404.vscode-proto3"
},
// ESLint // ESLint
"eslint.workingDirectories": ["./client"], "eslint.workingDirectories": ["./client"],

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Trev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

83
README.md Normal file
View File

@ -0,0 +1,83 @@
## TrevStack
This is a CRUD app to use as a template for starting projects
### Features
- **Communicate anywhere**. Define a [protocol buffer](https://protobuf.dev/), and [Connect](https://connectrpc.com/) generates type-safe code to facilitate communication between the server and any client (web, mobile, embedded, etc). The protocol buffers can contain annotations to validate fields on the client and server. For clients that cannot use Connect, an OpenAPI spec is also generated
- **Build anywhere**. The dev environment, testing and building is all declared in a single [Nix](https://nixos.org/) flake. Every developer and server can use the same environment
- **Deploy anywhere**. CI/CD is already set up using github actions. New versions are automatically released for every major platform, along with a docker image. The binaries created require zero run-time dependencies and are relatively small (this app is 26 MiB)
- Authentication is rolled in, including API key, fingerprint & passkey
- Automatic database migration on startup
- Light & dark modes with the [catppuccin](https://catppuccin.com/palette/) color palette
- Really good at running as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
While I personally prefer using a Svelte frontend and Go backend, feel free to swap them out with whatever you like, you'll be surprised how easy it is.
## Getting Started
1. [Install Nix](https://nixos.org/download/)
2. Run `nix develop`
3. Create a `server/.env` file that looks something like
```env
KEY=changeme
PORT=8080
URL=http://localhost:5173
DATABASE_URL=sqlite:/home/trev/.config/trevstack/sqlite.db
```
4. Run `treli`
It's that simple. If you're feeling fancy, install [direnv](https://direnv.net/) and the dev environment will load automatically.
### Useful Commands
- `nix run #update`: updates all of the dependencies
- `nix run #bump [major | minor]`: bumps the current version up one. Defaults to "patch" (0.0.1 -> 0.0.2)
- `buf lint` & `buf generate`: Lints and generates code from protocol buffers
- `sqlc vet` & `sqlc generate`: Verifies and generates code from SQL files
- `dbmate new` & `dbmate up`: Creates a new migration file and runs pending migrations
## Components
### Client
- **svelte 5** [[docs](https://svelte.dev/docs/svelte)] UI framework
- **tailwind 4** [[docs](https://tailwindcss.com/)] CSS framework
- **bits ui** [[docs](https://bits-ui.com/docs/)] headless components
- [components](https://github.com/spotdemo4/trevstack/tree/main/client/src/lib/ui) from **shadcn-svelte** [[docs](https://www.shadcn-svelte.com/docs)] altered to work with tailwind 4, fit the [catppuccin](https://catppuccin.com/palette/) color palette, and use [shallow routing](https://svelte.dev/docs/kit/shallow-routing)
- **connect rpc** [[docs](https://connectrpc.com/docs/web/)] to communicating with the server
- **protovalidate-es** [[docs](https://github.com/bufbuild/protovalidate-es)], along with a function [coolforms](https://github.com/spotdemo4/trevstack/blob/main/client/src/lib/coolforms/) to emulate the library [sveltekit-superforms](https://superforms.rocks/)
- **simplewebauthn** [[docs](https://simplewebauthn.dev/docs/packages/browser)] for passkey authentication
- **scalar** [[docs](https://github.com/scalar/scalar)] for displaying openapi specs
- **tw-animate-css** [[docs](https://github.com/Wombosvideo/tw-animate-css)] to animate with just tailwind classes
- vite [[docs](https://vite.dev/)] for dev server and bundling
- eslint [[docs](https://eslint.org/)] for linting
- prettier [[docs](https://prettier.io/)] for formatting
- prettier-plugin-svelte [[docs](https://github.com/sveltejs/prettier-plugin-svelte)]
- prettier-plugin-tailwindcss [[docs](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)]
- prettier-plugin-sort-imports [[docs](https://github.com/IanVS/prettier-plugin-sort-imports)]
### Server
- **go** [[docs](https://go.dev/doc/)]
- **connect rpc** [[docs](https://connectrpc.com/docs/go/)] to serve gRPC & HTTP requests
- **protovalidate-go** [[docs](https://github.com/bufbuild/protovalidate-go)] for validating those requests
- **sqlc** [[docs](https://docs.sqlc.dev/en/latest/)] because writing the SQL yourself is better than an ORM
- **go-webauthn** [[docs](https://github.com/go-webauthn/webauthn)] because webauthn is hard
- **dbmate** [[docs](https://github.com/amacneil/dbmate)] for database migrations
- revive [[docs](https://github.com/mgechev/revive)] for linting
### Protocol Buffers / gRPC
- **buf** [[docs](https://buf.build/docs/)] CLI for linting & code generation
- **protovalidate** [[docs](https://buf.build/docs/protovalidate/)] provides annotations to validate proto messages & fields
- **protoc-gen-connect-openapi** [[docs](https://github.com/sudorandom/protoc-gen-connect-openapi)] generates openapi specs
- protoc-gen-go [[docs](https://pkg.go.dev/google.golang.org/protobuf)]
- protoc-gen-connect-go [[docs](https://connectrpc.com/docs/go)]
- protoc-gen-es [[docs](https://connectrpc.com/docs/web/)]

View File

@ -27,5 +27,5 @@ plugins:
out: client/static/openapi out: client/static/openapi
strategy: all strategy: all
opt: opt:
- base=openapi.base.yaml - base=openapi.yaml
- path=openapi.yaml - path=openapi.yaml

View File

@ -1,12 +1,12 @@
{ {
"name": "trevstack", "name": "trevstack",
"version": "0.0.23", "version": "0.0.25",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trevstack", "name": "trevstack",
"version": "0.0.23", "version": "0.0.25",
"devDependencies": { "devDependencies": {
"@bufbuild/protovalidate": "^0.1.1", "@bufbuild/protovalidate": "^0.1.1",
"@connectrpc/connect": "^2.0.2", "@connectrpc/connect": "^2.0.2",
@ -26,7 +26,6 @@
"eslint": "^9.26.0", "eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.6.0", "eslint-plugin-svelte": "^3.6.0",
"fast-deep-equal": "^3.1.3",
"globals": "^16.1.0", "globals": "^16.1.0",
"mode-watcher": "^1.0.7", "mode-watcher": "^1.0.7",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@ -2754,9 +2753,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.46", "version": "20.17.47",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.47.tgz",
"integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", "integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,7 +1,7 @@
{ {
"name": "trevstack", "name": "trevstack",
"private": true, "private": true,
"version": "0.0.23", "version": "0.0.25",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -32,7 +32,6 @@
"eslint": "^9.26.0", "eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.6.0", "eslint-plugin-svelte": "^3.6.0",
"fast-deep-equal": "^3.1.3",
"globals": "^16.1.0", "globals": "^16.1.0",
"mode-watcher": "^1.0.7", "mode-watcher": "^1.0.7",
"prettier": "^3.5.3", "prettier": "^3.5.3",

View File

@ -21,7 +21,7 @@
... ...
}: let }: let
pname = "trevstack"; pname = "trevstack";
version = "0.0.23"; version = "0.0.25";
build-systems = [ build-systems = [
"x86_64-linux" "x86_64-linux"
@ -29,6 +29,17 @@
"x86_64-darwin" "x86_64-darwin"
"aarch64-darwin" "aarch64-darwin"
]; ];
forSystem = f:
nixpkgs.lib.genAttrs build-systems (
system:
f {
inherit system;
pkgs = import nixpkgs {
inherit system;
};
}
);
host-systems = [ host-systems = [
{ {
GOOS = "linux"; GOOS = "linux";
@ -55,16 +66,6 @@
GOARCH = "arm64"; GOARCH = "arm64";
} }
]; ];
forSystem = f:
nixpkgs.lib.genAttrs build-systems (
system:
f {
inherit system;
pkgs = import nixpkgs {
inherit system;
};
}
);
in { in {
devShells = forSystem ({pkgs, ...}: let devShells = forSystem ({pkgs, ...}: let
protoc-gen-connect-openapi = pkgs.buildGoModule { protoc-gen-connect-openapi = pkgs.buildGoModule {
@ -80,9 +81,9 @@
in { in {
default = pkgs.mkShell { default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
treli.packages."${system}".default
git git
nix-update nix-update
treli.packages."${system}".default
# Server # Server
go go
@ -126,7 +127,7 @@
pname = "check-client"; pname = "check-client";
inherit version; inherit version;
src = ./client; src = ./client;
npmDepsHash = "sha256-Te5HGbp7mKG3p1P4O266IpoPPBN7oQ/dZbttdgKbgWs="; npmDepsHash = "sha256-IVTm9gQx2ceQoqyJfIz1PJeq3/yb/3NSgMNTIWSsDMQ=";
dontNpmInstall = true; dontNpmInstall = true;
buildPhase = '' buildPhase = ''
@ -188,7 +189,7 @@
client = pkgs.buildNpmPackage { client = pkgs.buildNpmPackage {
inherit pname version; inherit pname version;
src = ./client; src = ./client;
npmDepsHash = "sha256-Te5HGbp7mKG3p1P4O266IpoPPBN7oQ/dZbttdgKbgWs="; npmDepsHash = "sha256-IVTm9gQx2ceQoqyJfIz1PJeq3/yb/3NSgMNTIWSsDMQ=";
installPhase = '' installPhase = ''
cp -r build "$out" cp -r build "$out"

View File

@ -3,8 +3,8 @@ servers:
- url: /grpc - url: /grpc
info: info:
title: Trevstack API title: Trevstack API
version: 1.0.0 version: 0.0.25
description: API for trevstack description: API for Trevstack
contact: contact:
name: Trev name: Trev
email: spam@trev.xyz email: spam@trev.xyz

View File

@ -1,16 +0,0 @@
wipe: true
replacements:
- match:
db_type: "INTEGER"
replace: "int64"
- match:
db_type: "INTEGER"
nullable: true
replace: "int64"
sql:
dialect: sqlite
dir: db
output: internal/models

View File

@ -29,6 +29,8 @@ import (
) )
func main() { func main() {
name := "TrevStack"
// Get env // Get env
env, err := getEnv() env, err := getEnv()
if err != nil { if err != nil {
@ -49,7 +51,7 @@ func main() {
// Create webauthn // Create webauthn
webAuthn, err := webauthn.New(&webauthn.Config{ webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: env.Name, RPDisplayName: name,
RPID: env.URL.Hostname(), RPID: env.URL.Hostname(),
RPOrigins: []string{env.URL.String()}, RPOrigins: []string{env.URL.String()},
}) })
@ -63,9 +65,9 @@ func main() {
log.Fatalf("failed to create validator: %s", err.Error()) log.Fatalf("failed to create validator: %s", err.Error())
} }
// Serve GRPC Handlers // Serve gRPC Handlers
api := http.NewServeMux() api := http.NewServeMux()
api.Handle(interceptors.WithCORS(user.NewAuthHandler(vi, sqlc, webAuthn, env.Name, env.Key))) api.Handle(interceptors.WithCORS(user.NewAuthHandler(vi, sqlc, webAuthn, name, env.Key)))
api.Handle(interceptors.WithCORS(user.NewHandler(vi, sqlc, webAuthn, env.Key))) api.Handle(interceptors.WithCORS(user.NewHandler(vi, sqlc, webAuthn, env.Key)))
api.Handle(interceptors.WithCORS(item.NewHandler(vi, sqlc, env.Key))) api.Handle(interceptors.WithCORS(item.NewHandler(vi, sqlc, env.Key)))
@ -108,7 +110,6 @@ func main() {
type env struct { type env struct {
Port string Port string
Key string Key string
Name string
URL *url.URL URL *url.URL
DatabaseURL string DatabaseURL string
} }
@ -123,7 +124,6 @@ func getEnv() (*env, error) {
env := env{ env := env{
Port: os.Getenv("PORT"), Port: os.Getenv("PORT"),
Key: os.Getenv("KEY"), Key: os.Getenv("KEY"),
Name: os.Getenv("NAME"),
DatabaseURL: os.Getenv("DATABASE_URL"), DatabaseURL: os.Getenv("DATABASE_URL"),
} }
@ -135,10 +135,6 @@ func getEnv() (*env, error) {
if env.Key == "" { if env.Key == "" {
return nil, errors.New("env 'KEY' not found") return nil, errors.New("env 'KEY' not found")
} }
if env.Name == "" {
env.Name = "trevstack"
log.Printf("env 'NAME' not found, defaulting to %s\n", env.Name)
}
if env.DatabaseURL == "" { if env.DatabaseURL == "" {
return nil, errors.New("env 'DATABASE_URL' not found") return nil, errors.New("env 'DATABASE_URL' not found")
} }