fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
  2. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
  3. fastapi_gen/__init__.py +6 -1
  4. fastapi_gen/cli.py +9 -0
  5. fastapi_gen/config.py +154 -2
  6. fastapi_gen/generator.py +34 -14
  7. fastapi_gen/prompts.py +172 -31
  8. fastapi_gen/template/VARIABLES.md +33 -4
  9. fastapi_gen/template/cookiecutter.json +10 -0
  10. fastapi_gen/template/hooks/post_gen_project.py +87 -2
  11. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
  12. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
  69. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
  70. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
  71. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
fastapi_gen/generator.py CHANGED
@@ -12,31 +12,31 @@ from .config import DatabaseType, FrontendType, ProjectConfig
12
12
  console = Console()
13
13
 
14
14
 
15
- def _get_database_setup_commands(database: DatabaseType) -> list[str]:
15
+ def _get_database_setup_commands(database: DatabaseType) -> list[tuple[str, str]]:
16
16
  """Get database-specific setup commands for post-generation messages.
17
17
 
18
18
  Args:
19
19
  database: The database type selected by the user
20
20
 
21
21
  Returns:
22
- List of command strings to display
22
+ List of (command, description) tuples to display
23
23
  """
24
24
  if database == DatabaseType.SQLITE:
25
25
  return [
26
- "# SQLite database will be created automatically at first run",
27
- "make db-migrate # Create initial migration",
28
- "make db-upgrade # Apply migrations",
26
+ ("# SQLite database will be created automatically", ""),
27
+ ("make db-migrate", "Create initial migration"),
28
+ ("make db-upgrade", "Apply migrations"),
29
29
  ]
30
30
  elif database == DatabaseType.MONGODB:
31
31
  return [
32
- "make docker-mongo # Start MongoDB container",
33
- "# Or configure MongoDB Atlas connection in .env",
32
+ ("make docker-mongo", "Start MongoDB container"),
33
+ ("# Or configure MongoDB Atlas connection in .env", ""),
34
34
  ]
35
35
  else: # PostgreSQL
36
36
  return [
37
- "make docker-db # Start PostgreSQL container",
38
- "make db-migrate # Create initial migration",
39
- "make db-upgrade # Apply migrations",
37
+ ("make docker-db", "Start PostgreSQL container"),
38
+ ("make db-migrate", "Create initial migration"),
39
+ ("make db-upgrade", "Apply migrations"),
40
40
  ]
41
41
 
42
42
 
@@ -153,8 +153,18 @@ def post_generation_tasks(project_path: Path, config: ProjectConfig) -> None:
153
153
  if config.database.value != "none":
154
154
  console.print()
155
155
  console.print(f"[bold]{step}. Database setup:[/]")
156
- for cmd in _get_database_setup_commands(config.database):
157
- console.print(f" {cmd}")
156
+ db_commands = _get_database_setup_commands(config.database)
157
+ for cmd, desc in db_commands:
158
+ if desc:
159
+ console.print(f" {cmd:22}# {desc}")
160
+ else:
161
+ console.print(f" {cmd}")
162
+ if config.database != DatabaseType.MONGODB:
163
+ console.print()
164
+ console.print(
165
+ " [dim]⚠️ Run all commands in order:[/] "
166
+ "[dim]db-migrate creates the migration, db-upgrade applies it[/]"
167
+ )
158
168
  step += 1
159
169
  console.print()
160
170
  console.print(f"[bold]{step}. Run backend:[/]")
@@ -184,8 +194,18 @@ def post_generation_tasks(project_path: Path, config: ProjectConfig) -> None:
184
194
  if config.database.value != "none":
185
195
  console.print()
186
196
  console.print(f"[bold]{step}. Database setup:[/]")
187
- for cmd in _get_database_setup_commands(config.database):
188
- console.print(f" {cmd}")
197
+ db_commands = _get_database_setup_commands(config.database)
198
+ for cmd, desc in db_commands:
199
+ if desc:
200
+ console.print(f" {cmd:22}# {desc}")
201
+ else:
202
+ console.print(f" {cmd}")
203
+ if config.database != DatabaseType.MONGODB:
204
+ console.print()
205
+ console.print(
206
+ " [dim]⚠️ Run all commands in order:[/] "
207
+ "[dim]db-migrate creates the migration, db-upgrade applies it[/]"
208
+ )
189
209
  step += 1
190
210
  console.print()
191
211
  console.print(f"[bold]{step}. Run server:[/]")
fastapi_gen/prompts.py CHANGED
@@ -19,6 +19,7 @@ from .config import (
19
19
  LLMProviderType,
20
20
  LogfireFeatures,
21
21
  OAuthProvider,
22
+ OrmType,
22
23
  ProjectConfig,
23
24
  RateLimitStorageType,
24
25
  ReverseProxyType,
@@ -152,6 +153,31 @@ def prompt_database() -> DatabaseType:
152
153
  )
153
154
 
154
155
 
156
+ def prompt_orm_type() -> OrmType:
157
+ """Prompt for ORM library selection."""
158
+ choices = [
159
+ questionary.Choice(
160
+ "SQLAlchemy — full control, supports admin panel",
161
+ value=OrmType.SQLALCHEMY,
162
+ ),
163
+ questionary.Choice(
164
+ "SQLModel — less boilerplate, no admin panel support",
165
+ value=OrmType.SQLMODEL,
166
+ ),
167
+ ]
168
+
169
+ return cast(
170
+ OrmType,
171
+ _check_cancelled(
172
+ questionary.select(
173
+ "ORM Library:",
174
+ choices=choices,
175
+ default=choices[0],
176
+ ).ask()
177
+ ),
178
+ )
179
+
180
+
155
181
  def prompt_auth() -> AuthType:
156
182
  """Prompt for authentication method."""
157
183
  console.print()
@@ -263,36 +289,120 @@ def prompt_background_tasks() -> BackgroundTaskType:
263
289
  )
264
290
 
265
291
 
266
- def prompt_integrations() -> dict[str, bool]:
267
- """Prompt for optional integrations."""
292
+ def prompt_integrations(
293
+ database: DatabaseType,
294
+ orm_type: OrmType,
295
+ ) -> dict[str, bool]:
296
+ """Prompt for optional integrations.
297
+
298
+ Args:
299
+ database: Selected database type (affects which options are shown).
300
+ orm_type: Selected ORM type (SQLModel doesn't support admin panel).
301
+ """
268
302
  console.print()
269
303
  console.print("[bold cyan]Optional Integrations[/]")
270
304
  console.print()
271
305
 
306
+ # Build choices dynamically based on context
307
+ choices: list[questionary.Choice] = [
308
+ questionary.Choice(
309
+ "Redis — required for caching, rate limiting (Redis), task queues",
310
+ value="redis",
311
+ ),
312
+ questionary.Choice(
313
+ "Caching (fastapi-cache2) — requires Redis",
314
+ value="caching",
315
+ ),
316
+ questionary.Choice(
317
+ "Rate limiting (slowapi) — optional Redis storage",
318
+ value="rate_limiting",
319
+ ),
320
+ questionary.Choice(
321
+ "Pagination (fastapi-pagination)",
322
+ value="pagination",
323
+ checked=True,
324
+ ),
325
+ questionary.Choice(
326
+ "Sentry — error tracking & monitoring",
327
+ value="sentry",
328
+ ),
329
+ questionary.Choice(
330
+ "Prometheus — metrics endpoint for monitoring",
331
+ value="prometheus",
332
+ ),
333
+ ]
334
+
335
+ # Admin Panel only available with SQLAlchemy (not SQLModel) and SQL database
336
+ if (
337
+ database in (DatabaseType.POSTGRESQL, DatabaseType.SQLITE)
338
+ and orm_type == OrmType.SQLALCHEMY
339
+ ):
340
+ choices.append(
341
+ questionary.Choice(
342
+ "Admin Panel (SQLAdmin) — web UI for database management",
343
+ value="admin_panel",
344
+ )
345
+ )
346
+
347
+ choices.extend(
348
+ [
349
+ questionary.Choice(
350
+ "WebSockets — real-time bidirectional communication",
351
+ value="websockets",
352
+ ),
353
+ questionary.Choice(
354
+ "File Storage (S3/MinIO) — file upload/download support",
355
+ value="file_storage",
356
+ ),
357
+ questionary.Choice(
358
+ "AI Agent (PydanticAI/LangGraph/CrewAI) — LLM-powered assistant",
359
+ value="ai_agent",
360
+ checked=True,
361
+ ),
362
+ ]
363
+ )
364
+
365
+ # Webhooks require database
366
+ if database != DatabaseType.NONE:
367
+ choices.append(
368
+ questionary.Choice(
369
+ "Webhooks — outbound event notifications",
370
+ value="webhooks",
371
+ )
372
+ )
373
+
374
+ choices.extend(
375
+ [
376
+ questionary.Choice(
377
+ "Example CRUD (Item model) — sample API endpoints",
378
+ value="example_crud",
379
+ checked=True,
380
+ ),
381
+ questionary.Choice(
382
+ "CORS middleware — cross-origin request support",
383
+ value="cors",
384
+ checked=True,
385
+ ),
386
+ questionary.Choice(
387
+ "orjson — faster JSON serialization",
388
+ value="orjson",
389
+ checked=True,
390
+ ),
391
+ ]
392
+ )
393
+
272
394
  features = _check_cancelled(
273
395
  questionary.checkbox(
274
396
  "Select additional features:",
275
- choices=[
276
- questionary.Choice("Redis (caching/sessions)", value="redis"),
277
- questionary.Choice("Caching (fastapi-cache2)", value="caching"),
278
- questionary.Choice("Rate limiting (slowapi)", value="rate_limiting"),
279
- questionary.Choice(
280
- "Pagination (fastapi-pagination)", value="pagination", checked=True
281
- ),
282
- questionary.Choice("Sentry (error tracking)", value="sentry"),
283
- questionary.Choice("Prometheus (metrics)", value="prometheus"),
284
- questionary.Choice("Admin Panel (SQLAdmin)", value="admin_panel"),
285
- questionary.Choice("WebSockets", value="websockets"),
286
- questionary.Choice("File Storage (S3/MinIO)", value="file_storage"),
287
- questionary.Choice("AI Agent (PydanticAI/LangChain)", value="ai_agent"),
288
- questionary.Choice("Webhooks (outbound events)", value="webhooks"),
289
- questionary.Choice("Example CRUD (Item model)", value="example_crud", checked=True),
290
- questionary.Choice("CORS middleware", value="cors", checked=True),
291
- questionary.Choice("orjson (faster JSON)", value="orjson", checked=True),
292
- ],
397
+ choices=choices,
293
398
  ).ask()
294
399
  )
295
400
 
401
+ # Auto-enable Redis for caching (show info message)
402
+ if "caching" in features and "redis" not in features:
403
+ console.print("[yellow]ℹ Caching requires Redis — auto-enabled[/]")
404
+ features.append("redis")
405
+
296
406
  return {
297
407
  "enable_redis": "redis" in features,
298
408
  "enable_caching": "caching" in features,
@@ -426,7 +536,14 @@ def prompt_reverse_proxy() -> ReverseProxyType:
426
536
  "Traefik (included in docker-compose)", value=ReverseProxyType.TRAEFIK_INCLUDED
427
537
  ),
428
538
  questionary.Choice(
429
- "External Traefik (shared between projects)", value=ReverseProxyType.TRAEFIK_EXTERNAL
539
+ "Traefik (external, shared between projects)",
540
+ value=ReverseProxyType.TRAEFIK_EXTERNAL,
541
+ ),
542
+ questionary.Choice(
543
+ "Nginx (included in docker-compose)", value=ReverseProxyType.NGINX_INCLUDED
544
+ ),
545
+ questionary.Choice(
546
+ "Nginx (external, config template only)", value=ReverseProxyType.NGINX_EXTERNAL
430
547
  ),
431
548
  questionary.Choice(
432
549
  "None (expose ports directly, use own proxy)", value=ReverseProxyType.NONE
@@ -452,8 +569,8 @@ def prompt_frontend() -> FrontendType:
452
569
  console.print()
453
570
 
454
571
  choices = [
455
- questionary.Choice("None (API only)", value=FrontendType.NONE),
456
572
  questionary.Choice("Next.js 15 (App Router, TypeScript, Bun)", value=FrontendType.NEXTJS),
573
+ questionary.Choice("None (API only)", value=FrontendType.NONE),
457
574
  ]
458
575
 
459
576
  return cast(
@@ -497,6 +614,9 @@ def prompt_ai_framework() -> AIFrameworkType:
497
614
  choices = [
498
615
  questionary.Choice("PydanticAI (recommended)", value=AIFrameworkType.PYDANTIC_AI),
499
616
  questionary.Choice("LangChain", value=AIFrameworkType.LANGCHAIN),
617
+ questionary.Choice("LangGraph (ReAct agent)", value=AIFrameworkType.LANGGRAPH),
618
+ questionary.Choice("CrewAI (multi-agent crews)", value=AIFrameworkType.CREWAI),
619
+ questionary.Choice("DeepAgents (agentic coding)", value=AIFrameworkType.DEEPAGENTS),
500
620
  ]
501
621
 
502
622
  return cast(
@@ -516,7 +636,7 @@ def prompt_llm_provider(ai_framework: AIFrameworkType) -> LLMProviderType:
516
636
 
517
637
  Args:
518
638
  ai_framework: The selected AI framework. OpenRouter is only
519
- available for PydanticAI.
639
+ available for PydanticAI (not LangChain, LangGraph, CrewAI, or DeepAgents).
520
640
  """
521
641
  console.print()
522
642
  console.print("[bold cyan]LLM Provider[/]")
@@ -527,7 +647,7 @@ def prompt_llm_provider(ai_framework: AIFrameworkType) -> LLMProviderType:
527
647
  questionary.Choice("Anthropic (claude-sonnet-4-5)", value=LLMProviderType.ANTHROPIC),
528
648
  ]
529
649
 
530
- # OpenRouter only available for PydanticAI
650
+ # OpenRouter only available for PydanticAI (not LangChain, LangGraph, or CrewAI)
531
651
  if ai_framework == AIFrameworkType.PYDANTIC_AI:
532
652
  choices.append(
533
653
  questionary.Choice("OpenRouter (multi-provider)", value=LLMProviderType.OPENROUTER)
@@ -545,18 +665,31 @@ def prompt_llm_provider(ai_framework: AIFrameworkType) -> LLMProviderType:
545
665
  )
546
666
 
547
667
 
548
- def prompt_websocket_auth() -> WebSocketAuthType:
549
- """Prompt for WebSocket authentication method for AI Agent."""
668
+ def prompt_websocket_auth(auth: AuthType) -> WebSocketAuthType:
669
+ """Prompt for WebSocket authentication method for AI Agent.
670
+
671
+ Args:
672
+ auth: The main auth type. JWT WebSocket auth is only available
673
+ if JWT is enabled (JWT or BOTH).
674
+ """
550
675
  console.print()
551
676
  console.print("[bold cyan]AI Agent WebSocket Authentication[/]")
552
677
  console.print()
553
678
 
554
679
  choices = [
555
680
  questionary.Choice("None (public access)", value=WebSocketAuthType.NONE),
556
- questionary.Choice("JWT token required", value=WebSocketAuthType.JWT),
557
- questionary.Choice("API Key required (query param)", value=WebSocketAuthType.API_KEY),
558
681
  ]
559
682
 
683
+ # JWT WebSocket auth only available if main auth uses JWT
684
+ if auth in (AuthType.JWT, AuthType.BOTH):
685
+ choices.append(questionary.Choice("JWT token required", value=WebSocketAuthType.JWT))
686
+
687
+ # API Key WebSocket auth available if main auth uses API keys
688
+ if auth in (AuthType.API_KEY, AuthType.BOTH):
689
+ choices.append(
690
+ questionary.Choice("API Key required (query param)", value=WebSocketAuthType.API_KEY)
691
+ )
692
+
560
693
  return cast(
561
694
  WebSocketAuthType,
562
695
  _check_cancelled(
@@ -679,6 +812,11 @@ def run_interactive_prompts() -> ProjectConfig:
679
812
  # Database
680
813
  database = prompt_database()
681
814
 
815
+ # ORM type (only for PostgreSQL or SQLite)
816
+ orm_type = OrmType.SQLALCHEMY
817
+ if database in (DatabaseType.POSTGRESQL, DatabaseType.SQLITE):
818
+ orm_type = prompt_orm_type()
819
+
682
820
  # Auth
683
821
  auth = prompt_auth()
684
822
 
@@ -702,8 +840,8 @@ def run_interactive_prompts() -> ProjectConfig:
702
840
  # Background tasks
703
841
  background_tasks = prompt_background_tasks()
704
842
 
705
- # Integrations
706
- integrations = prompt_integrations()
843
+ # Integrations (pass context for dynamic option filtering)
844
+ integrations = prompt_integrations(database=database, orm_type=orm_type)
707
845
 
708
846
  # Dev tools
709
847
  dev_tools = prompt_dev_tools()
@@ -738,7 +876,7 @@ def run_interactive_prompts() -> ProjectConfig:
738
876
  if integrations.get("enable_ai_agent"):
739
877
  ai_framework = prompt_ai_framework()
740
878
  llm_provider = prompt_llm_provider(ai_framework)
741
- websocket_auth = prompt_websocket_auth()
879
+ websocket_auth = prompt_websocket_auth(auth=auth)
742
880
  # Only offer persistence if database is enabled
743
881
  if database != DatabaseType.NONE:
744
882
  enable_conversation_persistence = _check_cancelled(
@@ -781,6 +919,7 @@ def run_interactive_prompts() -> ProjectConfig:
781
919
  author_name=basic_info["author_name"],
782
920
  author_email=basic_info["author_email"],
783
921
  database=database,
922
+ orm_type=orm_type,
784
923
  auth=auth,
785
924
  oauth_provider=oauth_provider,
786
925
  enable_session_management=enable_session_management,
@@ -818,6 +957,8 @@ def show_summary(config: ProjectConfig) -> None:
818
957
 
819
958
  console.print(f" [cyan]Project:[/] {config.project_name}")
820
959
  console.print(f" [cyan]Database:[/] {config.database.value}")
960
+ if config.database in (DatabaseType.POSTGRESQL, DatabaseType.SQLITE):
961
+ console.print(f" [cyan]ORM:[/] {config.orm_type.value}")
821
962
  auth_str = config.auth.value
822
963
  if config.oauth_provider != OAuthProvider.NONE:
823
964
  auth_str += f" + {config.oauth_provider.value} OAuth"
@@ -59,6 +59,19 @@ These variables are set automatically by the generator.
59
59
  | `db_max_overflow` | int | `10` | Max overflow connections above pool size | Requires SQL database |
60
60
  | `db_pool_timeout` | int | `30` | Timeout (seconds) waiting for connection | Requires SQL database |
61
61
 
62
+ ### ORM Library
63
+
64
+ | Variable | Type | Default | Description | Dependencies |
65
+ |----------|------|---------|-------------|--------------|
66
+ | `orm_type` | enum | `"sqlalchemy"` | ORM library. Values: `sqlalchemy`, `sqlmodel` | Requires SQL database |
67
+ | `use_sqlalchemy` | bool | `true` | SQLAlchemy is selected | Computed from `orm_type` |
68
+ | `use_sqlmodel` | bool | `false` | SQLModel is selected | Computed from `orm_type` |
69
+
70
+ **Notes:**
71
+ - SQLModel provides simplified syntax combining SQLAlchemy and Pydantic
72
+ - SQLModel is only available for PostgreSQL and SQLite (not MongoDB)
73
+ - SQLModel uses the same database session and migrations as SQLAlchemy
74
+
62
75
  **Notes:**
63
76
  - PostgreSQL uses `asyncpg` for async operations
64
77
  - MongoDB uses `motor` for async operations
@@ -174,9 +187,12 @@ These variables are set automatically by the generator.
174
187
  | Variable | Type | Default | Description | Dependencies |
175
188
  |----------|------|---------|-------------|--------------|
176
189
  | `enable_ai_agent` | bool | `false` | Enable AI agent functionality | - |
177
- | `ai_framework` | enum | `"pydantic_ai"` | AI framework. Values: `pydantic_ai`, `langchain` | Requires `enable_ai_agent` |
190
+ | `ai_framework` | enum | `"pydantic_ai"` | AI framework. Values: `pydantic_ai`, `langchain`, `langgraph`, `crewai`, `deepagents` | Requires `enable_ai_agent` |
178
191
  | `use_pydantic_ai` | bool | `true` | PydanticAI is selected | Computed from `ai_framework` |
179
192
  | `use_langchain` | bool | `false` | LangChain is selected | Computed from `ai_framework` |
193
+ | `use_langgraph` | bool | `false` | LangGraph (ReAct agent) is selected | Computed from `ai_framework` |
194
+ | `use_crewai` | bool | `false` | CrewAI (multi-agent crews) is selected | Computed from `ai_framework` |
195
+ | `use_deepagents` | bool | `false` | DeepAgents (agentic coding) is selected | Computed from `ai_framework` |
180
196
  | `llm_provider` | enum | `"openai"` | LLM provider. Values: `openai`, `anthropic`, `openrouter` | Requires `enable_ai_agent` |
181
197
  | `use_openai` | bool | `true` | OpenAI is selected | Computed from `llm_provider` |
182
198
  | `use_anthropic` | bool | `false` | Anthropic is selected | Computed from `llm_provider` |
@@ -185,7 +201,10 @@ These variables are set automatically by the generator.
185
201
 
186
202
  **Notes:**
187
203
  - PydanticAI uses `iter()` for full event streaming over WebSocket
188
- - OpenRouter with LangChain is not supported
204
+ - LangGraph implements a ReAct (Reasoning + Acting) agent pattern with graph-based architecture
205
+ - CrewAI enables multi-agent teams that collaborate on complex tasks
206
+ - DeepAgents provides an agentic coding assistant with built-in filesystem tools (ls, read_file, write_file, edit_file, glob, grep) and task management
207
+ - OpenRouter with LangChain, LangGraph, CrewAI, or DeepAgents is not supported
189
208
 
190
209
  ---
191
210
 
@@ -221,14 +240,20 @@ These variables are set automatically by the generator.
221
240
  | `use_github_actions` | bool | `true` | GitHub Actions is selected | Computed from `ci_type` |
222
241
  | `use_gitlab_ci` | bool | `false` | GitLab CI is selected | Computed from `ci_type` |
223
242
  | `enable_kubernetes` | bool | `false` | Include Kubernetes manifests | - |
224
- | `reverse_proxy` | enum | `"traefik_included"` | Reverse proxy config. Values: `traefik_included`, `traefik_external`, `none` | Requires Docker |
243
+ | `reverse_proxy` | enum | `"traefik_included"` | Reverse proxy config. Values: `traefik_included`, `traefik_external`, `nginx_included`, `nginx_external`, `none` | Requires Docker |
225
244
  | `include_traefik_service` | bool | `true` | Include Traefik container in docker-compose | Computed from `reverse_proxy` |
226
245
  | `include_traefik_labels` | bool | `true` | Include Traefik labels on services | Computed from `reverse_proxy` |
246
+ | `use_traefik` | bool | `true` | Using Traefik (included or external) | Computed from `reverse_proxy` |
247
+ | `include_nginx_service` | bool | `false` | Include Nginx container in docker-compose | Computed from `reverse_proxy` |
248
+ | `include_nginx_config` | bool | `false` | Generate nginx configuration files | Computed from `reverse_proxy` |
249
+ | `use_nginx` | bool | `false` | Using Nginx (included or external) | Computed from `reverse_proxy` |
227
250
 
228
251
  **Reverse Proxy Options:**
229
252
  - `traefik_included`: Full Traefik setup included in docker-compose.prod.yml (default)
230
253
  - `traefik_external`: Services have Traefik labels but no Traefik container (for shared Traefik)
231
- - `none`: No reverse proxy, ports exposed directly (use your own nginx/caddy/HAProxy)
254
+ - `nginx_included`: Full Nginx setup included in docker-compose.prod.yml with config template
255
+ - `nginx_external`: Nginx config template only, for external Nginx (no container in compose)
256
+ - `none`: No reverse proxy, ports exposed directly (use your own proxy)
232
257
 
233
258
  ---
234
259
 
@@ -265,6 +290,10 @@ database = "postgresql"
265
290
  → use_mongodb = false
266
291
  → use_sqlite = false
267
292
  → use_database = true
293
+
294
+ orm_type = "sqlmodel"
295
+ → use_sqlalchemy = false
296
+ → use_sqlmodel = true
268
297
  ```
269
298
 
270
299
  These computed variables are used in Jinja2 conditionals within templates:
@@ -16,6 +16,9 @@
16
16
  "db_pool_size": 5,
17
17
  "db_max_overflow": 10,
18
18
  "db_pool_timeout": 30,
19
+ "orm_type": "sqlalchemy",
20
+ "use_sqlalchemy": true,
21
+ "use_sqlmodel": false,
19
22
  "auth": "jwt",
20
23
  "use_jwt": true,
21
24
  "use_api_key": false,
@@ -58,6 +61,9 @@
58
61
  "ai_framework": "pydantic_ai",
59
62
  "use_pydantic_ai": true,
60
63
  "use_langchain": false,
64
+ "use_langgraph": false,
65
+ "use_crewai": false,
66
+ "use_deepagents": false,
61
67
  "llm_provider": "openai",
62
68
  "use_openai": true,
63
69
  "use_anthropic": false,
@@ -79,6 +85,10 @@
79
85
  "reverse_proxy": "traefik_included",
80
86
  "include_traefik_service": true,
81
87
  "include_traefik_labels": true,
88
+ "use_traefik": true,
89
+ "include_nginx_service": false,
90
+ "include_nginx_config": false,
91
+ "use_nginx": false,
82
92
  "ci_type": "github",
83
93
  "use_github_actions": true,
84
94
  "use_gitlab_ci": false,
@@ -16,10 +16,15 @@ use_database = "{{ cookiecutter.use_database }}" == "True"
16
16
  use_postgresql = "{{ cookiecutter.use_postgresql }}" == "True"
17
17
  use_sqlite = "{{ cookiecutter.use_sqlite }}" == "True"
18
18
  use_mongodb = "{{ cookiecutter.use_mongodb }}" == "True"
19
+ use_sqlalchemy = "{{ cookiecutter.use_sqlalchemy }}" == "True"
20
+ use_sqlmodel = "{{ cookiecutter.use_sqlmodel }}" == "True"
19
21
  include_example_crud = "{{ cookiecutter.include_example_crud }}" == "True"
20
22
  enable_ai_agent = "{{ cookiecutter.enable_ai_agent }}" == "True"
21
23
  use_pydantic_ai = "{{ cookiecutter.use_pydantic_ai }}" == "True"
22
24
  use_langchain = "{{ cookiecutter.use_langchain }}" == "True"
25
+ use_langgraph = "{{ cookiecutter.use_langgraph }}" == "True"
26
+ use_crewai = "{{ cookiecutter.use_crewai }}" == "True"
27
+ use_deepagents = "{{ cookiecutter.use_deepagents }}" == "True"
23
28
  enable_admin_panel = "{{ cookiecutter.enable_admin_panel }}" == "True"
24
29
  enable_websockets = "{{ cookiecutter.enable_websockets }}" == "True"
25
30
  enable_redis = "{{ cookiecutter.enable_redis }}" == "True"
@@ -30,9 +35,15 @@ enable_conversation_persistence = "{{ cookiecutter.enable_conversation_persisten
30
35
  enable_webhooks = "{{ cookiecutter.enable_webhooks }}" == "True"
31
36
  enable_oauth = "{{ cookiecutter.enable_oauth }}" == "True"
32
37
  use_jwt = "{{ cookiecutter.use_jwt }}" == "True"
38
+ use_api_key = "{{ cookiecutter.use_api_key }}" == "True"
33
39
  use_celery = "{{ cookiecutter.use_celery }}" == "True"
34
40
  use_taskiq = "{{ cookiecutter.use_taskiq }}" == "True"
35
41
  use_arq = "{{ cookiecutter.use_arq }}" == "True"
42
+ use_github_actions = "{{ cookiecutter.use_github_actions }}" == "True"
43
+ use_gitlab_ci = "{{ cookiecutter.use_gitlab_ci }}" == "True"
44
+ enable_kubernetes = "{{ cookiecutter.enable_kubernetes }}" == "True"
45
+ use_nginx = "{{ cookiecutter.use_nginx }}" == "True"
46
+ enable_logfire = "{{ cookiecutter.enable_logfire }}" == "True"
36
47
 
37
48
 
38
49
  def remove_file(path: str) -> None:
@@ -42,6 +53,23 @@ def remove_file(path: str) -> None:
42
53
  print(f" Removed: {os.path.relpath(path)}")
43
54
 
44
55
 
56
+ def is_stub_file(filepath: str) -> bool:
57
+ """Check if file only contains a docstring stub with no real code."""
58
+ if not os.path.exists(filepath):
59
+ return False
60
+ with open(filepath) as f:
61
+ content = f.read().strip()
62
+ # Empty file
63
+ if not content:
64
+ return True
65
+ # File only has docstring (triple-quoted string)
66
+ if content.startswith('"""') and content.endswith('"""'):
67
+ # Check if there's only one docstring and no code
68
+ inner = content[3:-3].strip()
69
+ return '"""' not in inner and "def " not in content and "class " not in content
70
+ return False
71
+
72
+
45
73
  def remove_dir(path: str) -> None:
46
74
  """Remove a directory if it exists."""
47
75
  if os.path.exists(path):
@@ -68,6 +96,12 @@ else:
68
96
  remove_file(os.path.join(backend_app, "agents", "assistant.py"))
69
97
  if not use_langchain:
70
98
  remove_file(os.path.join(backend_app, "agents", "langchain_assistant.py"))
99
+ if not use_langgraph:
100
+ remove_file(os.path.join(backend_app, "agents", "langgraph_assistant.py"))
101
+ if not use_crewai:
102
+ remove_file(os.path.join(backend_app, "agents", "crewai_assistant.py"))
103
+ if not use_deepagents:
104
+ remove_file(os.path.join(backend_app, "agents", "deepagents_assistant.py"))
71
105
 
72
106
  # --- Example CRUD files ---
73
107
  if not include_example_crud or not use_database:
@@ -105,8 +139,8 @@ if not enable_session_management or not use_jwt:
105
139
  if not enable_websockets:
106
140
  remove_file(os.path.join(backend_app, "api", "routes", "v1", "ws.py"))
107
141
 
108
- # --- Admin panel ---
109
- if not enable_admin_panel or (not use_postgresql and not use_sqlite):
142
+ # --- Admin panel (requires SQLAlchemy, not SQLModel) ---
143
+ if not enable_admin_panel or (not use_postgresql and not use_sqlite) or not use_sqlalchemy:
110
144
  remove_file(os.path.join(backend_app, "admin.py"))
111
145
 
112
146
  # --- Redis/Cache files ---
@@ -125,6 +159,32 @@ if not enable_oauth:
125
159
  remove_file(os.path.join(backend_app, "api", "routes", "v1", "oauth.py"))
126
160
  remove_file(os.path.join(backend_app, "core", "oauth.py"))
127
161
 
162
+ # --- Security file (only when no auth at all) ---
163
+ if not use_jwt and not use_api_key:
164
+ remove_file(os.path.join(backend_app, "core", "security.py"))
165
+
166
+ # --- Auth/User files (when JWT is disabled) ---
167
+ if not use_jwt:
168
+ remove_file(os.path.join(backend_app, "api", "routes", "v1", "auth.py"))
169
+ remove_file(os.path.join(backend_app, "api", "routes", "v1", "users.py"))
170
+ remove_file(os.path.join(backend_app, "db", "models", "user.py"))
171
+ remove_file(os.path.join(backend_app, "repositories", "user.py"))
172
+ remove_file(os.path.join(backend_app, "services", "user.py"))
173
+ remove_file(os.path.join(backend_app, "schemas", "user.py"))
174
+ remove_file(os.path.join(backend_app, "schemas", "token.py"))
175
+
176
+ # --- Logfire setup file (when logfire is disabled) ---
177
+ if not enable_logfire:
178
+ remove_file(os.path.join(backend_app, "core", "logfire_setup.py"))
179
+
180
+ # --- Cleanup stub files (files with only docstring, no code) ---
181
+ core_dir = os.path.join(backend_app, "core")
182
+ for stub_candidate in ["security.py", "cache.py", "rate_limit.py", "oauth.py", "logfire_setup.py", "csrf.py"]:
183
+ filepath = os.path.join(core_dir, stub_candidate)
184
+ if is_stub_file(filepath):
185
+ remove_file(filepath)
186
+ print(f" Removed stub: {os.path.relpath(filepath)}")
187
+
128
188
  # --- Worker/Background tasks ---
129
189
  use_any_background_tasks = use_celery or use_taskiq or use_arq
130
190
  if not use_any_background_tasks:
@@ -172,6 +232,31 @@ for subdir in ["clients", "agents", "worker", "worker/tasks"]:
172
232
 
173
233
  print("File cleanup complete.")
174
234
 
235
+ # --- CI/CD files cleanup ---
236
+ if not use_github_actions:
237
+ github_dir = os.path.join(os.getcwd(), ".github")
238
+ if os.path.exists(github_dir):
239
+ shutil.rmtree(github_dir)
240
+ print("Removed .github/ directory (GitHub Actions not enabled)")
241
+
242
+ if not use_gitlab_ci:
243
+ gitlab_ci_file = os.path.join(os.getcwd(), ".gitlab-ci.yml")
244
+ if os.path.exists(gitlab_ci_file):
245
+ os.remove(gitlab_ci_file)
246
+ print("Removed .gitlab-ci.yml (GitLab CI not enabled)")
247
+
248
+ if not enable_kubernetes:
249
+ kubernetes_dir = os.path.join(os.getcwd(), "kubernetes")
250
+ if os.path.exists(kubernetes_dir):
251
+ shutil.rmtree(kubernetes_dir)
252
+ print("Removed kubernetes/ directory (Kubernetes not enabled)")
253
+
254
+ if not use_nginx:
255
+ nginx_dir = os.path.join(os.getcwd(), "nginx")
256
+ if os.path.exists(nginx_dir):
257
+ shutil.rmtree(nginx_dir)
258
+ print("Removed nginx/ directory (Nginx not enabled)")
259
+
175
260
  # Remove frontend folder if not using frontend
176
261
  if not use_frontend:
177
262
  frontend_dir = os.path.join(os.getcwd(), "frontend")
@@ -17,6 +17,15 @@ DOMAIN=example.com
17
17
  ACME_EMAIL=admin@example.com
18
18
  {%- endif %}
19
19
 
20
+ {%- if cookiecutter.use_nginx %}
21
+
22
+ # === Domain Configuration ===
23
+ DOMAIN=example.com
24
+ # Note: Place SSL certificates in nginx/ssl/ directory:
25
+ # - nginx/ssl/cert.pem (certificate chain)
26
+ # - nginx/ssl/key.pem (private key)
27
+ {%- endif %}
28
+
20
29
  {%- if cookiecutter.use_postgresql %}
21
30
 
22
31
  # === PostgreSQL ===