Skip to content
Merged

Shop #58

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
52485db
refactor(infra): pin the shared network name and add healthchecks
bahman026 Aug 6, 2026
ca8ac06
refactor(shop): give the docker services unique names
bahman026 Aug 6, 2026
c61ec97
refactor(admin): give the docker services unique names
bahman026 Aug 6, 2026
3df6ca6
chore(shop): document SHOP_CONTAINER_PREFIX
bahman026 Aug 6, 2026
872b110
chore(admin): document ADMIN_CONTAINER_PREFIX
bahman026 Aug 6, 2026
c1541a6
fix(shop): point nginx at the renamed php-fpm service
bahman026 Aug 6, 2026
223268a
fix(admin): point nginx at the renamed php-fpm service
bahman026 Aug 6, 2026
0e62187
feat: start every development container from the repository root
bahman026 Aug 6, 2026
af7b49a
chore(admin): add a .dockerignore for the production build context
bahman026 Aug 6, 2026
bb0dd68
chore(shop): add a .dockerignore for the production build context
bahman026 Aug 6, 2026
355b502
feat(admin): add production php.ini with tuned opcache
bahman026 Aug 6, 2026
750ebff
feat(admin): add a production php-fpm pool configuration
bahman026 Aug 6, 2026
33ae90c
feat(admin): add a hardened production nginx configuration
bahman026 Aug 6, 2026
32cd301
feat(admin): add a production entrypoint that builds caches
bahman026 Aug 6, 2026
2e93804
feat(shop): add production php.ini with tuned opcache
bahman026 Aug 6, 2026
f4a7245
feat(shop): add a production php-fpm pool configuration
bahman026 Aug 6, 2026
4e070b6
feat(shop): add a hardened production nginx configuration
bahman026 Aug 6, 2026
92ff8c3
feat(shop): add a production entrypoint that builds caches
bahman026 Aug 6, 2026
83c58dd
feat(admin): add a multi-stage production image
bahman026 Aug 6, 2026
fff2302
feat(shop): add a multi-stage production image with an SSR target
bahman026 Aug 6, 2026
ee0b6e0
feat: add the production compose stack
bahman026 Aug 6, 2026
f9071c0
feat(infra): terminate TLS for both domains with Caddy
bahman026 Aug 6, 2026
518b725
feat(infra): add a deploy script with migrations as a release step
bahman026 Aug 6, 2026
2d1e48a
docs(infra): document deploying to an Ubuntu VPS
bahman026 Aug 6, 2026
00f88a5
chore: add the compose-level production env template
bahman026 Aug 6, 2026
206ef19
chore(admin): add the production env template
bahman026 Aug 6, 2026
f0f8172
chore(shop): add the production env template
bahman026 Aug 6, 2026
ebdf09a
fix(admin): trust the reverse proxy headers
bahman026 Aug 6, 2026
44fa61b
chore: ignore the root production env file
bahman026 Aug 6, 2026
c2968fa
docs: document the root compose file and the production stack
bahman026 Aug 6, 2026
ec08f94
feat(infra): build production images off-box and ship them over SSH
bahman026 Aug 20, 2026
6b57515
fix(admin): document seeding the first admin user with a role
bahman026 Aug 20, 2026
e7ff2f5
feat(infra): terminate TLS with a manually-issued certificate
bahman026 Aug 20, 2026
9e8543e
docs(infra): document off-box builds, manual TLS, and the admin-seed fix
bahman026 Aug 20, 2026
b000a8e
feat(admin)!: remove the admin-composed home page
bahman026 Aug 20, 2026
1490a01
feat(admin): show where each banner/slider position lands
bahman026 Aug 20, 2026
0c40969
feat(admin): gate every Filament resource behind a permission
bahman026 Aug 20, 2026
60d3ef0
fix(admin): constrain categories.parent_id with a foreign key
bahman026 Aug 20, 2026
4bfcb10
feat(admin): keep inventory in step with order status changes
bahman026 Aug 20, 2026
81ce3ba
docs: record that admin status changes now restock
bahman026 Aug 20, 2026
ec5bf46
feat(shop): render every banner and slider position
bahman026 Aug 20, 2026
b420aee
feat(shop): show featured tags as product carousels
bahman026 Aug 20, 2026
0f560bd
feat(shop): charge the coupon discount at checkout
bahman026 Aug 20, 2026
c4ad1ff
test(shop): fail the build when an enum drifts from admin's
bahman026 Aug 20, 2026
e10ef26
docs: move the AI context into ai-context/ and load it per app
bahman026 Aug 20, 2026
8f51507
docs: correct claims the recent commits made false
bahman026 Aug 20, 2026
be647fa
feat(shop): send login OTP codes over sms.ir
bahman026 Aug 20, 2026
f7a7bbf
feat(admin): add Pulse and a log viewer, gated to super-admin
bahman026 Aug 20, 2026
d1a028a
feat(admin): link Pulse and the log viewer from the panel sidebar
bahman026 Aug 20, 2026
97daaf5
docs: record the fish-shell and Filament-cache traps
bahman026 Aug 20, 2026
4b64b91
feat(shop): allow a fixed login code for the sandbox window
bahman026 Aug 20, 2026
517d943
feat(admin): constrain uploads to the shape each image slot actually …
bahman026 Aug 28, 2026
1fc9bbb
feat(admin): seed تیپاکس as a pay-on-delivery shipping method
bahman026 Aug 28, 2026
de1d45d
fix(shop): group مشخصات کالا spec rows by attribute instead of repeat…
bahman026 Aug 28, 2026
2e55437
feat: add a reproducible demo catalog dataset for local/staging seeding
bahman026 Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"timeout": 5,
"command": "f=\"$CLAUDE_PROJECT_DIR/ai-context/claude/personal.md\"; [ -f \"$f\" ] && cat \"$f\"; true"
}
]
}
]
}
}
39 changes: 39 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Compose-level values for compose.prod.yaml. Copy to .env.production on the
# VPS and fill in. These are read by Compose itself (via --env-file), not by
# the Laravel apps — those have their own admin/.env.production and
# shop/.env.production.

# --- Postgres --------------------------------------------------------------
# Only used the first time the pgdata volume is created. Changing them later
# does not change the existing role or database.
POSTGRES_DB=shop_flow
POSTGRES_USER=shop_flow
POSTGRES_PASSWORD=

# --- Redis -----------------------------------------------------------------
REDIS_PASSWORD=

# --- Public hostnames ------------------------------------------------------
# Both must resolve to this server. Caddy does not request a certificate for
# them itself — see infrastructure/production/certs/README.md — so DNS only
# needs to be correct by the time clients connect, not before Caddy starts.
SHOP_DOMAIN=shop.example.com
ADMIN_DOMAIN=admin.example.com

# Optional, space-separated. Old/alternate hostnames for the storefront that
# should 301-redirect to SHOP_DOMAIN instead of serving anything themselves —
# e.g. a bare apex when SHOP_DOMAIN is a subdomain, or vice versa after moving
# it. Must be included as SANs on the certificate in
# infrastructure/production/certs/ (see that directory's README) or the
# redirect itself fails as a certificate error. Leave unset for none.
SHOP_LEGACY_DOMAINS=

# Space-separated CIDRs whose X-Forwarded-* headers Caddy should trust, or the
# token `private_ranges`. Only matters if a CDN or LB sits in front of Caddy;
# `private_ranges` is correct when clients connect to Caddy directly.
TRUSTED_PROXIES=private_ranges

# --- Images ----------------------------------------------------------------
# Tag applied to the built images. Set it to the deployed commit
# (`git rev-parse --short HEAD`) so a rollback has something to point at.
IMAGE_TAG=latest
22 changes: 21 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
/.idea
/.idea
/.env.production

# macOS finder metadata, at any depth
.DS_Store

# TLS certificate + key for the production proxy, obtained per-deployment
# via infrastructure/production/certs/README.md.
/infrastructure/production/certs/*.pem

# Per-person AI context, never committed — each colleague keeps their own.
# Injected by the UserPromptSubmit hook in .claude/settings.json.
/ai-context/claude/personal.md

# Demo/staging catalog dataset. The authored content (demo/data/*.json) and
# the fetch scripts (demo/scripts/) ARE tracked, so the demo is reproducible
# on any machine — only the regenerable fetch cache and the Pexels API key
# stay out of git. See demo/README.md.
/demo/raw/
/demo/.env
/demo/scripts/node_modules/
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@ai-context/claude/CLAUDE.md
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The admin panel covers the full schema today. The storefront is built feature by

```
ShopFlow/
├── compose.yaml # Root entry point: brings up all six containers
├── admin/ # Filament admin panel (owns the DB schema)
├── shop/ # Inertia + Vue storefront (SSR)
├── infrastructure/
Expand Down Expand Up @@ -54,16 +55,49 @@ Run migrations and seeders from `admin/` only. The storefront must not migrate t

## Getting started

### 1. Shared services (Postgres + Redis)
### 1. Docker environment files

In `infrastructure/docker`, create a `.env` from `.env.example`, then start the containers:
Each compose file reads its own `.env`. Create all three from their examples:

```bash
cd infrastructure/docker
sudo docker compose up -d --build
cp infrastructure/docker/.env.example infrastructure/docker/.env
cp admin/docker/.env.example admin/docker/.env
cp shop/docker/.env.example shop/docker/.env
```

### 2. Configure each app
Fill in the blanks in `infrastructure/docker/.env` (database name, user, password,
Redis password) and set `USER_ID`/`GROUP_ID` to your own (`id -u`, `id -g`).

### 2. Start every container

The root `compose.yaml` merges the three compose files into one project, so a
single command from the repository root brings up the shared services and both
applications:

```bash
docker compose up -d --build
```

That starts six containers on a shared `shop_flow_net` network:

| Container | Role | Host port |
| --- | --- | --- |
| `shop_flow_db` | PostgreSQL 16 | `127.0.0.1:5432` |
| `shop_flow_redis` | Redis | `127.0.0.1:6379` |
| `shop_flow_admin_app` | admin PHP-FPM | — |
| `shop_flow_admin_nginx` | admin web server | `127.0.0.1:4040` |
| `shop_flow_shop_app` | storefront PHP-FPM | — |
| `shop_flow_shop_nginx` | storefront web server | `127.0.0.1:8080` |

Both apps wait for Postgres and Redis to report healthy before they start. Host
ports come from the `*_EXPOSE_PORT` variables in the three `.env` files.

Each app can still be started on its own — `docker compose up -d` inside
`infrastructure/docker`, `admin/docker`, or `shop/docker`. In that mode the
`infrastructure` project must come up first, because it creates the
`shop_flow_net` network that the other two join as an external network.

### 3. Configure each app

In both `admin/.env` and `shop/.env`, point the database at the shared Postgres (matching the values from `infrastructure/docker/.env`):

Expand All @@ -74,7 +108,7 @@ DB_PORT=5432
# DB_DATABASE / DB_USERNAME / DB_PASSWORD must match infrastructure/docker/.env
```

### 3. Admin (schema owner — set up first)
### 4. Admin (schema owner — set up first)

```bash
cd admin
Expand All @@ -84,7 +118,7 @@ php artisan migrate --seed
npm install && npm run build
```

### 4. Storefront
### 5. Storefront

```bash
cd shop
Expand All @@ -95,6 +129,18 @@ npm install && npm run build

For app-specific details (Docker containers, SSR, conventions), see each app's own `README.md`, `AGENTS.md`, and `docs/`.

## Production

`compose.yaml` is for development only — it bind-mounts the source and installs
dependencies on every container start. Production uses a separate stack,
`compose.prod.yaml`, which bakes the application into images, serves both apps
through Caddy with automatic TLS, and runs the Inertia renderer as its own
container.

Setup for an Ubuntu VPS is documented in
[`infrastructure/production/README.md`](infrastructure/production/README.md);
deploys run through `./infrastructure/production/deploy.sh`.

## Testing & quality

The storefront bundles all checks into one command (run inside its container):
Expand Down
28 changes: 28 additions & 0 deletions admin/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Build context for docker/Dockerfile.prod. Anything listed here is rebuilt
# inside the image, so shipping the host's copy would only invalidate layers.
.git
.gitignore
.dockerignore
docker/volumes
node_modules
vendor
public/build
public/hot
public/storage
bootstrap/cache/*.php
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
.env
.env.*
!.env.example
tests
.phpunit.cache
.phpunit.result.cache
.idea
.vscode
.fleet
.junie
.ai
.claude
100 changes: 100 additions & 0 deletions admin/.env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Runtime environment for the admin containers. Copy to admin/.env.production
# on the VPS and fill in. Never committed — it holds real credentials.
#
# This file is passed to the container as environment variables; there is no
# .env inside the image. The entrypoint runs `config:cache` at start, so every
# change here needs a container restart to take effect.

APP_NAME=ShopFlow
APP_ENV=production
# php artisan key:generate --show
APP_KEY=
APP_DEBUG=false
APP_URL=https://admin.example.com

APP_LOCALE=fa
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=12

# Docker captures stderr, so there is no log file on disk to rotate.
# Both, deliberately: stderr keeps `docker logs` and Docker's own rotation
# working exactly as before, while `shared` writes the rotating file the admin
# panel's log viewer reads. LOG_SHARED_PATH is on a volume both app containers
# mount, one directory per app, because the panel cannot see into another
# container.
LOG_CHANNEL=stack
LOG_STACK=stderr,shared
LOG_SHARED_PATH=/var/log/shopflow/admin/laravel.log
LOG_DAILY_DAYS=14
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=warning

# Host names are the compose service names on the internal network.
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=shop_flow
DB_USERNAME=shop_flow
DB_PASSWORD=

REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=

# Redis rather than the database: sessions and cache are the hottest small
# reads in the panel, and Postgres should not be paying for them.
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
# Cookies are only ever sent over the Caddy TLS listener.
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax

CACHE_STORE=redis
CACHE_PREFIX=

# sync until the workers profile is started; then set this to redis.
QUEUE_CONNECTION=sync

BROADCAST_CONNECTION=log

# Uploads land in storage/app/public, which is the admin_storage volume and the
# path admin_web serves at /storage. The storefront reads them from there.
FILESYSTEM_DISK=public
FILAMENT_FILESYSTEM_DISK=public

# --- First admin account ---------------------------------------------------
# Read by AdminSeeder, which creates the account and assigns it the super-admin
# role. Set these before running `db:seed --class=Database\Seeders\AdminSeeder`
# and the defaults in config/admin.php (admin@shopFlow.dev / password) never
# reach production. Use AdminSeeder rather than `make:filament-user`: the panel
# gate is `canAccessPanel()`, which requires a role, and make:filament-user
# assigns none — the user it creates cannot log in.
ADMIN_FIRST_NAME=
ADMIN_LAST_NAME=
ADMIN_EMAIL=
ADMIN_PASSWORD=

MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_SCHEME=tls
MAIL_FROM_ADDRESS="noreply@example.com"
MAIL_FROM_NAME="${APP_NAME}"

# --- Operational dashboards ------------------------------------------------
# Both are gated to super-admin in AppServiceProvider; they expose slow
# queries, exception messages and full stack traces.
# /pulse — app + server health
# /log-viewer — both apps' logs, one folder each
# The storefront's logs arrive on the shared volume, not in this container.
LOG_VIEWER_SHOP_PATH=/var/log/shopflow/shop
PULSE_ENABLED=true
PULSE_SERVER_NAME=shopflow-prod
Loading
Loading