diff --git a/ai/shared.md b/ai/shared.md
index 00a1273..97f3e33 100644
--- a/ai/shared.md
+++ b/ai/shared.md
@@ -39,39 +39,14 @@ Use judgment for changes that cannot meaningfully be test-driven, such as
documentation-only edits or declarative environment configuration. Validate
those changes with the most relevant parser, formatter, dry run, or check.
-## Approval discipline
-
-- Treat dependency installation, tests, static analysis, formatting, linting,
- type checking, and builds as routine project operations. Run them without
- asking for advance approval when they stay within the requested scope and
- sandbox boundaries.
-- If an essential operation is blocked by sandbox or network policy, batch the
- necessary provisioning or validation into one narrowly scoped approval
- request. Prefer a reusable command prefix when it can be safely limited to
- the required tool and operation.
-- Do not request repeated approvals for commands that fit a previously
- approved scope.
-- Do not prefix routine commands with per-command cache environment overrides.
- Configure cache locations once in Codex, direnv, or the project environment.
- Use temporary overrides only to diagnose a cache-specific problem.
-- Truly destructive actions, external writes, main-stack control, and
- operations outside the workspace still require explicit approval.
-
## Running processes
- The main checkout owns the canonical stack on the default ports. Assume it
is the user's stack. Do not start, restart, or stop it unless the user asks.
- A worktree owns its own isolated stack. The flake shell hook assigns a
deterministic port offset and creates worktree-local PostgreSQL state.
-- Start a worktree stack only when runtime or integration validation requires
- it. Start it once from the worktree root, reuse it throughout validation,
- and stop it once when finished.
-- Do not start any service for PHPUnit, frontend formatting, linting, type
- checking, or production builds.
-- When authorized worktree stack control is necessary, operate it directly.
- Do not ask the user to start or stop worktree services.
-- For non-interactive use, start the stack detached and stop it when finished,
- as shown below.
+- Start a worktree stack from its root. For non-interactive use, start it
+ detached and stop it when finished, as shown below.
- Do not use `process-compose -t=false` for a detached stack. It can leave an
orphaned PostgreSQL process holding the data directory.
- Non-interactive agent shells do not automatically load direnv. Bare project
diff --git a/backend/.env.example b/backend/.env.example
index 027ae21..c3214a6 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -3,7 +3,6 @@ APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=https://localhost:8000
-FRONTEND_URL=https://localhost:5173
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php
deleted file mode 100644
index de82e1a..0000000
--- a/backend/app/Http/Controllers/AuthController.php
+++ /dev/null
@@ -1,23 +0,0 @@
-attributes->get('user');
-
- return new JsonResponse([
- 'user' => [
- 'id' => $user->getId(),
- 'email' => $user->getEmail()->value(),
- ],
- ]);
- }
-}
diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php
index 141b221..324b7c2 100644
--- a/backend/bootstrap/app.php
+++ b/backend/bootstrap/app.php
@@ -9,7 +9,6 @@ use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
- api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
diff --git a/backend/config/cors.php b/backend/config/cors.php
deleted file mode 100644
index 5189eb1..0000000
--- a/backend/config/cors.php
+++ /dev/null
@@ -1,22 +0,0 @@
- ['api/*'],
- 'allowed_methods' => ['*'],
- 'allowed_origins' => $allowedOrigins,
- 'allowed_origins_patterns' => [],
- 'allowed_headers' => ['*'],
- 'exposed_headers' => [],
- 'max_age' => 0,
- 'supports_credentials' => true,
-];
diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php
index def224c..dc2b6db 100644
--- a/backend/database/seeders/DatabaseSeeder.php
+++ b/backend/database/seeders/DatabaseSeeder.php
@@ -11,6 +11,5 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
- $this->call(UserSeeder::class);
}
}
diff --git a/backend/database/seeders/UserSeeder.php b/backend/database/seeders/UserSeeder.php
deleted file mode 100644
index 886e4c9..0000000
--- a/backend/database/seeders/UserSeeder.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'user@example.com',
- ]);
- }
-}
diff --git a/backend/phpunit.xml b/backend/phpunit.xml
index dafc3f1..e7f0a48 100644
--- a/backend/phpunit.xml
+++ b/backend/phpunit.xml
@@ -26,7 +26,6 @@
-
diff --git a/backend/routes/api.php b/backend/routes/api.php
deleted file mode 100644
index d8fba04..0000000
--- a/backend/routes/api.php
+++ /dev/null
@@ -1,9 +0,0 @@
-group(function (): void {
- Route::get('/me', [AuthController::class, 'me']);
-});
diff --git a/backend/tests/Feature/Auth/MeEndpointTest.php b/backend/tests/Feature/Auth/MeEndpointTest.php
deleted file mode 100644
index ec19fc3..0000000
--- a/backend/tests/Feature/Auth/MeEndpointTest.php
+++ /dev/null
@@ -1,74 +0,0 @@
-create(new CreateUserDto(
- email: new EmailAddress('user@example.com'),
- ));
- app(SessionRepository::class)->create(new CreateSessionDto(
- token: 'valid-token',
- user: $user,
- createdAt: $now,
- expiresAt: $now->modify('+7 days'),
- ));
-
- $response = $this->withCredentials()
- ->withUnencryptedCookie(
- AuthMiddleware::COOKIE_NAME,
- 'valid-token',
- )->getJson('/api/me');
-
- $response->assertOk()->assertExactJson([
- 'user' => [
- 'id' => $user->getId(),
- 'email' => 'user@example.com',
- ],
- ]);
- }
-
- public function test_me_rejects_a_request_without_a_cookie(): void
- {
- $response = $this->getJson('/api/me');
-
- $response
- ->assertStatus(401)
- ->assertExactJson(['error' => 'unauthenticated']);
- }
-
- public function test_me_allows_credentialed_frontend_requests(): void
- {
- $response = $this->withHeaders([
- 'Origin' => 'https://localhost:5173',
- 'Access-Control-Request-Method' => 'GET',
- ])->options('/api/me');
-
- $response
- ->assertNoContent()
- ->assertHeader(
- 'Access-Control-Allow-Origin',
- 'https://localhost:5173',
- )
- ->assertHeader('Access-Control-Allow-Credentials', 'true');
- }
-}
diff --git a/backend/tests/Feature/Database/DatabaseSeederTest.php b/backend/tests/Feature/Database/DatabaseSeederTest.php
deleted file mode 100644
index 26b6b06..0000000
--- a/backend/tests/Feature/Database/DatabaseSeederTest.php
+++ /dev/null
@@ -1,22 +0,0 @@
-seed();
- $this->seed();
-
- $this->assertDatabaseHas('users', [
- 'email' => 'user@example.com',
- ]);
- $this->assertDatabaseCount('users', 1);
- }
-}
diff --git a/frontend/website/cypress.config.ts b/frontend/website/cypress.config.ts
index 4a2d276..2697f45 100644
--- a/frontend/website/cypress.config.ts
+++ b/frontend/website/cypress.config.ts
@@ -5,7 +5,7 @@ const frontendPort = process.env.VITE_PORT ?? '5173'
export default defineConfig({
allowCypressEnv: false,
e2e: {
- baseUrl: `https://localhost:${frontendPort}`,
+ baseUrl: `http://127.0.0.1:${frontendPort}`,
supportFile: false,
},
video: false,
diff --git a/frontend/website/cypress/e2e/guest-auth.cy.ts b/frontend/website/cypress/e2e/guest-auth.cy.ts
index 2dea54b..f4c1ba8 100644
--- a/frontend/website/cypress/e2e/guest-auth.cy.ts
+++ b/frontend/website/cypress/e2e/guest-auth.cy.ts
@@ -8,15 +8,6 @@ describe('guest authentication pages', () => {
cy.contains('a', 'Get started').should('have.attr', 'href', '/signup')
})
- it('uses Attainly browser metadata', () => {
- cy.visit('/')
-
- cy.title().should('equal', 'Attainly')
- cy.get('link[rel="icon"]')
- .should('have.attr', 'type', 'image/svg+xml')
- .and('have.attr', 'href', '/favicon.svg')
- })
-
it('redirects guests away from the protected dashboard', () => {
cy.visit('/dashboard')
diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts
deleted file mode 100644
index d7fb723..0000000
--- a/frontend/website/cypress/e2e/session-auth.cy.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-const authenticatedUser = {
- id: 7,
- email: 'user@example.com',
-}
-
-describe('session authentication', () => {
- it('restores an authenticated session on a protected route', () => {
- cy.intercept('GET', '**/api/me', {
- statusCode: 200,
- body: { user: authenticatedUser },
- }).as('me')
-
- cy.visit('/dashboard')
- cy.wait('@me')
-
- cy.location('pathname').should('equal', '/dashboard')
- cy.get('h1').should('have.text', 'Your next step starts here.')
- })
-
- it('redirects an unauthenticated protected route to login', () => {
- cy.intercept('GET', '**/api/me', {
- statusCode: 401,
- body: { error: 'unauthenticated' },
- }).as('me')
-
- cy.visit('/dashboard')
- cy.wait('@me')
-
- cy.location('pathname').should('equal', '/login')
- cy.location('search').should('include', 'redirect=/dashboard')
- })
-
- it('redirects a restored session away from a guest-only route', () => {
- cy.intercept('GET', '**/api/me', {
- statusCode: 200,
- body: { user: authenticatedUser },
- }).as('me')
-
- cy.visit('/login')
- cy.wait('@me')
-
- cy.location('pathname').should('equal', '/dashboard')
- })
-
- it('rejects a malformed authenticated-user response', () => {
- cy.intercept('GET', '**/api/me', {
- statusCode: 200,
- body: { user: { id: 7 } },
- }).as('me')
-
- cy.visit('/dashboard')
- cy.wait('@me')
-
- cy.location('pathname').should('equal', '/login')
- })
-})
diff --git a/frontend/website/env.d.ts b/frontend/website/env.d.ts
index b54b4c9..11f02fe 100644
--- a/frontend/website/env.d.ts
+++ b/frontend/website/env.d.ts
@@ -1,9 +1 @@
///
-
-interface ImportMetaEnv {
- readonly VITE_API_URL: string
-}
-
-interface ImportMeta {
- readonly env: ImportMetaEnv
-}
diff --git a/frontend/website/index.html b/frontend/website/index.html
index ca72e36..9e5fc8f 100644
--- a/frontend/website/index.html
+++ b/frontend/website/index.html
@@ -2,9 +2,9 @@
-
+
- Attainly
+ Vite App
diff --git a/frontend/website/package-lock.json b/frontend/website/package-lock.json
index e47680a..6cbfc4d 100644
--- a/frontend/website/package-lock.json
+++ b/frontend/website/package-lock.json
@@ -10,8 +10,7 @@
"dependencies": {
"pinia": "^4.0.2",
"vue": "^3.5.40",
- "vue-router": "^5.2.0",
- "zod": "^4.4.3"
+ "vue-router": "^5.2.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
@@ -7621,15 +7620,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
- },
- "node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
}
}
}
diff --git a/frontend/website/package.json b/frontend/website/package.json
index e70648e..ebd0942 100644
--- a/frontend/website/package.json
+++ b/frontend/website/package.json
@@ -18,8 +18,7 @@
"dependencies": {
"pinia": "^4.0.2",
"vue": "^3.5.40",
- "vue-router": "^5.2.0",
- "zod": "^4.4.3"
+ "vue-router": "^5.2.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
diff --git a/frontend/website/public/favicon.ico b/frontend/website/public/favicon.ico
new file mode 100644
index 0000000..df36fcf
Binary files /dev/null and b/frontend/website/public/favicon.ico differ
diff --git a/frontend/website/public/favicon.svg b/frontend/website/public/favicon.svg
deleted file mode 100644
index 3d796bf..0000000
--- a/frontend/website/public/favicon.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
diff --git a/frontend/website/src/router/index.ts b/frontend/website/src/router/index.ts
index 51e7184..b726d17 100644
--- a/frontend/website/src/router/index.ts
+++ b/frontend/website/src/router/index.ts
@@ -37,31 +37,21 @@ const router = createRouter({
],
})
-router.beforeEach(async (to) => {
+router.beforeEach((to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
- const restoredSession = await authStore.fetchMe()
- if (!restoredSession) {
- return {
- name: 'login',
- query: {
- redirect: to.fullPath,
- },
- }
+ return {
+ name: 'login',
+ query: {
+ redirect: to.fullPath,
+ },
}
}
- if (to.meta.guestOnly) {
- let restoredSession = authStore.isAuthenticated
- if (!restoredSession) {
- restoredSession = await authStore.fetchMe()
- }
-
- if (restoredSession) {
- return {
- name: 'dashboard',
- }
+ if (to.meta.guestOnly && authStore.isAuthenticated) {
+ return {
+ name: 'dashboard',
}
}
})
diff --git a/frontend/website/src/stores/auth.ts b/frontend/website/src/stores/auth.ts
index 399a301..50688f1 100644
--- a/frontend/website/src/stores/auth.ts
+++ b/frontend/website/src/stores/auth.ts
@@ -1,67 +1,10 @@
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
import { defineStore } from 'pinia'
-import { z } from 'zod'
-
-import { API_BASE } from '@/utils/apiBase'
-
-export const authUserSchema = z.object({
- id: z.number().int().positive(),
- email: z.string().email(),
-})
-
-const meResponseSchema = z.object({
- user: authUserSchema,
-})
-
-export type AuthUser = z.infer
export const useAuthStore = defineStore('auth', () => {
- const user = ref(null)
- const loading = ref(false)
- const error = ref(null)
- const isAuthenticated = computed(() => user.value !== null)
-
- async function fetchMe(): Promise {
- loading.value = true
- error.value = null
-
- try {
- const response = await fetch(`${API_BASE}/api/me`, {
- method: 'GET',
- credentials: 'include',
- headers: {
- Accept: 'application/json',
- },
- })
-
- if (response.status === 200) {
- const responseBody: unknown = await response.json()
- user.value = meResponseSchema.parse(responseBody).user
-
- return true
- }
-
- user.value = null
- if (response.status !== 401) {
- error.value = 'Unable to restore session'
- }
-
- return false
- } catch {
- user.value = null
- error.value = 'Unable to restore session'
-
- return false
- } finally {
- loading.value = false
- }
- }
+ const isAuthenticated = ref(false)
return {
- user,
- loading,
- error,
isAuthenticated,
- fetchMe,
}
})
diff --git a/frontend/website/src/utils/apiBase.ts b/frontend/website/src/utils/apiBase.ts
deleted file mode 100644
index 0a1babc..0000000
--- a/frontend/website/src/utils/apiBase.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-function resolveApiBase(): string {
- const configuredApiUrl = import.meta.env.VITE_API_URL
- if (configuredApiUrl === '') {
- throw new Error('VITE_API_URL must be configured')
- }
-
- if (!import.meta.env.DEV) {
- return configuredApiUrl
- }
-
- const apiUrl = new URL(configuredApiUrl)
- apiUrl.hostname = window.location.hostname
-
- return apiUrl.origin
-}
-
-export const API_BASE = resolveApiBase()
diff --git a/frontend/website/vite.config.ts b/frontend/website/vite.config.ts
index a6fdcaa..ace127e 100644
--- a/frontend/website/vite.config.ts
+++ b/frontend/website/vite.config.ts
@@ -1,30 +1,12 @@
-import { existsSync, readFileSync } from 'node:fs'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
-import type { ServerOptions } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
-const certificateKeyPath = fileURLToPath(
- new URL('../../.cert/localhost-key.pem', import.meta.url),
-)
-const certificatePath = fileURLToPath(
- new URL('../../.cert/localhost.pem', import.meta.url),
-)
-const serverOptions: ServerOptions = {}
-
-if (existsSync(certificateKeyPath) && existsSync(certificatePath)) {
- serverOptions.https = {
- key: readFileSync(certificateKeyPath),
- cert: readFileSync(certificatePath),
- }
-}
-
// https://vite.dev/config/
export default defineConfig({
- server: serverOptions,
plugins: [
vue(),
vueJsx(),
diff --git a/nix/shell-hook.sh b/nix/shell-hook.sh
index 73d1874..d835131 100644
--- a/nix/shell-hook.sh
+++ b/nix/shell-hook.sh
@@ -38,7 +38,6 @@ export PGUSER="postgres"
export PGDATABASE="postgres"
DEV_APP_URL="https://localhost:$CADDY_PORT"
-DEV_FRONTEND_URL="https://localhost:$VITE_PORT"
DEV_DB_CONNECTION="pgsql"
DEV_DB_HOST="$PGHOST"
DEV_DB_PORT="5432"
@@ -100,7 +99,6 @@ set_env_value() {
}
set_env_value APP_URL "$DEV_APP_URL"
-set_env_value FRONTEND_URL "$DEV_FRONTEND_URL"
set_env_value DB_CONNECTION "$DEV_DB_CONNECTION"
set_env_value DB_HOST "$DEV_DB_HOST"
set_env_value DB_PORT "$DEV_DB_PORT"
@@ -111,13 +109,6 @@ set_env_value MAIL_MAILER "$DEV_MAIL_MAILER"
set_env_value MAIL_HOST "$DEV_MAIL_HOST"
set_env_value MAIL_PORT "$DEV_MAIL_PORT"
-FRONTEND_ENV_FILE="$REPO_ROOT/frontend/website/.env.local"
-FRONTEND_API_URL="VITE_API_URL=$DEV_APP_URL"
-if [ ! -f "$FRONTEND_ENV_FILE" ] \
- || [ "$(cat "$FRONTEND_ENV_FILE")" != "$FRONTEND_API_URL" ]; then
- printf '%s\n' "$FRONTEND_API_URL" > "$FRONTEND_ENV_FILE"
-fi
-
if [ ! -d "$REPO_ROOT/backend/vendor" ]; then
echo "[composer] installing backend dependencies"
(cd "$REPO_ROOT/backend" && composer install)
diff --git a/process-compose.yaml b/process-compose.yaml
index fcddf50..ad7a160 100644
--- a/process-compose.yaml
+++ b/process-compose.yaml
@@ -18,13 +18,6 @@ processes:
postgres:
condition: process_healthy
- seed:
- command: php artisan db:seed --force
- working_dir: ./backend
- depends_on:
- migrate:
- condition: process_completed_successfully
-
mailpit:
command: mailpit --smtp 127.0.0.1:${MAILPIT_SMTP_PORT:-2525} --listen 127.0.0.1:${MAILPIT_UI_PORT:-8025}
readiness_probe:
@@ -39,7 +32,7 @@ processes:
command: php artisan serve --host=127.0.0.1 --port=${BACKEND_PORT:-8001}
working_dir: ./backend
depends_on:
- seed:
+ migrate:
condition: process_completed_successfully
mailpit:
condition: process_healthy
@@ -63,7 +56,6 @@ processes:
host: 127.0.0.1
port: ${VITE_PORT:-5173}
path: /
- scheme: https
initial_delay_seconds: 1
period_seconds: 2