Skip to main content

Development Setup

Set up your development environment for maximum productivity with Aya’s monorepo structure.

Prerequisites

Before starting, complete the Installation guide for either Docker Compose or Nix.

IDE Configuration

Aya includes a .vscode directory with recommended settings and extensions.
.vscode/extensions.json
{
  "recommendations": [
    // Go
    "golang.go",
    "betteralign.betteralign",

    // Deno/TypeScript
    "denoland.vscode-deno",

    // General
    "editorconfig.editorconfig",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss",

    // Database
    "mtxr.sqltools",
    "mtxr.sqltools-driver-pg"
  ]
}
Install all recommended extensions:
  1. Open VS Code
  2. Press Cmd/Ctrl + Shift + P
  3. Type “Show Recommended Extensions”
  4. Click “Install All”

Workspace Settings

.vscode/settings.json
{
  // Deno configuration (for apps/webclient)
  "deno.enable": true,
  "deno.enablePaths": ["apps/webclient"],
  "deno.lint": true,
  "deno.unstable": false,

  // Go configuration (for apps/services)
  "go.useLanguageServer": true,
  "go.lintTool": "golangci-lint",
  "go.lintOnSave": "workspace",
  "go.buildOnSave": "off",
  "go.testOnSave": false,
  "go.coverOnSave": false,
  "go.formatTool": "gofumpt",
  "[go]": {
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": true
    }
  },

  // File associations
  "files.associations": {
    "*.sql": "sql",
    "*.mdx": "markdown"
  },

  // Tailwind
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ],

  // Exclude from file watcher
  "files.watcherExclude": {
    "**/.git/objects/**": true,
    "**/.git/subtree-cache/**": true,
    "**/node_modules/**": true,
    "**/.tanstack/**": true,
    "**/tmp/**": true
  }
}

Multi-Root Workspace

For better organization, use a multi-root workspace:
aya.code-workspace
{
  "folders": [
    { "path": ".", "name": "Root" },
    { "path": "apps/webclient", "name": "Frontend" },
    { "path": "apps/services", "name": "Backend" }
  ],
  "settings": {
    "deno.enablePaths": ["apps/webclient"],
    "go.goroot": "/usr/local/go"
  }
}
Open with: code aya.code-workspace

Project Structure Tour

aya.is/
├── apps/                     # Applications
│   ├── webclient/          # Frontend (Deno)
│   └── services/           # Backend (Go)
├── .github/                 # CI/CD workflows
├── .vscode/                 # VS Code settings
├── compose.yml              # Docker Compose config
├── flake.nix                # Nix development environment
├── Makefile                 # Root-level commands
├── .pre-commit-config.yaml  # Pre-commit hooks
└── README.md

Development Workflow

Starting Services

# Start all services
make up

# Watch mode (hot reload)
make watch

# View logs
make logs

# Restart after config changes
make restart services

Code Quality Checks

Run before committing:
# Run all checks (lint, format, test)
make ok

# Frontend only
cd apps/webclient
deno lint
deno fmt
deno task test:ci

# Backend only
cd apps/services
make lint
make test

Pre-commit Hooks

Install Git hooks to run checks automatically:
# Install pre-commit
pre-commit install

# Run hooks manually
pre-commit run --all-files
Configured hooks (.pre-commit-config.yaml):
  • Frontend: deno lint, deno fmt
  • Backend: golangci-lint, gofumpt
  • General: trailing whitespace, file size, merge conflicts

Hot Reload

Both frontend and backend support hot reload:

Frontend (Vite HMR)

Vite provides instant hot module replacement:
// Edit any file in src/
// Browser updates WITHOUT full page reload
What triggers HMR:
  • Component changes (.tsx)
  • CSS module changes (.module.css)
  • Route changes (src/routes/)
What triggers full reload:
  • Router configuration changes
  • deno.json changes
  • Vite config changes

Backend (Air)

Air watches Go files and rebuilds automatically:
.air.toml
[build]
  bin = "./tmp/serve"
  cmd = "go build -o ./tmp/serve ./cmd/serve/"
  include_ext = ["go", "tpl", "tmpl", "html"]
  exclude_dir = ["tmp", "vendor", "testdata"]
  delay = 1000  # 1 second debounce
What triggers rebuild:
  • .go file changes
  • Template changes (.tpl, .tmpl)
  • SQL changes (requires make generate first)

Database Development

Accessing PostgreSQL

# Via Docker
docker compose exec postgres psql -U postgres -d postgres

# Via local psql
psql postgres://postgres:s3cr3t@localhost:5432/postgres

Running Migrations

cd apps/services

# Apply all pending migrations
make migrate-up

# Rollback last migration
make migrate-down

# Check migration status
go run ./cmd/migrate/ default status

Creating Migrations

cd apps/services/etc/data/default/migrations

# Create new migration
touch 0011_add_profile_badges.sql
0011_add_profile_badges.sql
-- +goose Up
ALTER TABLE "profile" ADD COLUMN "badges" TEXT[];

CREATE INDEX profile_badges_idx ON "profile" USING GIN ("badges");

-- +goose Down
DROP INDEX profile_badges_idx;
ALTER TABLE "profile" DROP COLUMN "badges";
Apply:
make migrate-up

Code Generation (sqlc)

After creating/modifying SQL queries:
cd apps/services

# Generate Go code from SQL
make generate
# or directly:
sqlc generate
This generates type-safe Go code in pkg/api/adapters/*/db.go.

Testing

Frontend Tests

cd apps/webclient

# Run tests
deno task test

# Update snapshots
deno task test:update

# CI mode (read-only)
deno task test:ci
Tests use Deno’s built-in test runner:
src/lib/locale-utils.test.ts
import { assertEquals } from "@std/assert";
import { isValidLocale } from "./locale-utils.ts";

Deno.test("isValidLocale returns true for supported locales", () => {
  assertEquals(isValidLocale("en"), true);
  assertEquals(isValidLocale("tr"), true);
  assertEquals(isValidLocale("invalid"), false);
});

Backend Tests

cd apps/services

# Run all tests
make test

# Run with coverage
make test-cov

# View coverage HTML
make test-view-html

# Run specific package
go test ./pkg/api/business/profiles/...
Example test:
pkg/api/business/profiles/get_test.go
package profiles_test

import (
    "context"
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "aya.is/apps/services/pkg/api/business/profiles"
)

func TestGet_Success(t *testing.T) {
    mockRepo := new(MockRepository)
    ctx := context.Background()

    expectedProfile := &profiles.Profile{
        ID:    "profile-123",
        Slug:  "eser",
        Title: "Eser Ozvataf",
    }

    mockRepo.On("GetBySlug", ctx, "en", "eser").Return(expectedProfile, nil)

    result, err := profiles.Get(ctx, mockRepo, "en", "eser")

    assert.NoError(t, err)
    assert.Equal(t, expectedProfile, result)
    mockRepo.AssertExpectations(t)
}

Debugging

Frontend Debugging

Browser DevTools

  1. Open Chrome/Firefox DevTools (F12)
  2. Go to Sources tab
  3. Find src/ directory in file tree
  4. Set breakpoints
  5. Reload page

VS Code Debugger

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Chrome",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/apps/webclient/src"
    }
  ]
}

Backend Debugging

Delve (Go Debugger)

cd apps/services

# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest

# Start with debugger
dlv debug ./cmd/serve/ -- --config=config.json

# Set breakpoint
(dlv) break pkg/api/business/profiles/get.go:15
(dlv) continue

VS Code Debugger

.vscode/launch.json
{
  "configurations": [
    {
      "name": "Debug Backend",
      "type": "go",
      "request": "launch",
      "mode": "debug",
      "program": "${workspaceFolder}/apps/services/cmd/serve",
      "cwd": "${workspaceFolder}/apps/services",
      "env": {
        "AUTH__JWT_SECRET": "test-secret"
      }
    }
  ]
}
Set breakpoints in Go files, press F5.

Common Tasks

Add a New Route

1

Create route file

touch apps/webclient/src/routes/\$locale/new-page.tsx
2

Define route

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/$locale/new-page")({
  component: NewPage,
});

function NewPage() {
  return <h1>New Page</h1>;
}
3

Route is auto-registered

TanStack Router generates route tree automatically. Access at /en/new-page.

Add a New API Endpoint

1

Define business logic

pkg/api/business/profiles/delete.go
package profiles

func Delete(ctx context.Context, repo Repository, profileID, userID string) error {
    // Business rules here
    return repo.Delete(ctx, profileID)
}
2

Implement repository method

pkg/api/adapters/profiles/repository.go
func (r *Repository) Delete(ctx context.Context, profileID string) error {
    _, err := r.queries.DeleteProfile(ctx, profileID)
    return err
}
3

Add HTTP handler

pkg/api/adapters/http/profiles.go
func (h *ProfilesHandler) DeleteProfile(c *gin.Context) {
    profileID := c.Param("id")
    userID := c.GetString("user_id")

    err := profiles.Delete(c.Request.Context(), h.repo, profileID, userID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(204, nil)
}
4

Register route

pkg/api/adapters/http/router.go
router.DELETE("/profiles/:id", handler.DeleteProfile)

Troubleshooting

Ensure Deno extension is enabled for webclient:
"deno.enablePaths": ["apps/webclient"]
Reload VS Code window: Cmd/Ctrl + Shift + P → “Reload Window”
Run go mod tidy:
cd apps/services
go mod tidy
Restart Go language server in VS Code.
Frontend: Check terminal for Vite errors. Restart with Ctrl+C and deno task dev.Backend: Check Air logs. Verify .air.toml includes correct file extensions.
Ensure PostgreSQL is running:
docker compose ps postgres
# Should show "Up (healthy)"

# Restart if needed
docker compose restart postgres

Next Steps

Frontend Development

Build UI with Deno, TanStack Start, and React

Backend Development

Implement business logic and API endpoints

Database Guide

Work with PostgreSQL, migrations, and sqlc

Architecture

Understand the hexagonal architecture

Build docs developers (and LLMs) love