@apso/cli 0.5.0 → 0.7.0

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 (69) hide show
  1. package/README.md +510 -1
  2. package/dist/commands/server/new.js +1 -1
  3. package/dist/commands/server/scaffold.js +19 -10
  4. package/dist/lib/apsorc-parser.d.ts +3 -0
  5. package/dist/lib/apsorc-parser.js +11 -6
  6. package/dist/lib/controller.js +3 -1
  7. package/dist/lib/dto.js +9 -9
  8. package/dist/lib/entity.js +6 -5
  9. package/dist/lib/guards.d.ts +4 -3
  10. package/dist/lib/guards.js +64 -25
  11. package/dist/lib/module.js +3 -1
  12. package/dist/lib/templates/guards/auth.guard.eta +228 -0
  13. package/dist/lib/templates/guards/guards.module.eta +73 -13
  14. package/dist/lib/templates/guards/index.eta +18 -0
  15. package/dist/lib/templates/guards/scope.guard.eta +12 -4
  16. package/dist/lib/types/auth.d.ts +203 -0
  17. package/dist/lib/types/auth.js +60 -0
  18. package/dist/lib/types/entity.d.ts +2 -2
  19. package/dist/lib/types/index.d.ts +1 -0
  20. package/dist/lib/types/index.js +8 -0
  21. package/dist/lib/types/relationship.d.ts +1 -1
  22. package/dist/lib/utils/casing.js +5 -5
  23. package/dist/lib/utils/field.js +2 -1
  24. package/dist/lib/utils/file-system.js +1 -1
  25. package/dist/lib/utils/relationships/parse-v1.js +2 -1
  26. package/dist/lib/utils/relationships/parse.js +117 -32
  27. package/dist/lib/utils/relationships/parse.spec.js +168 -141
  28. package/dist/test-nested-relationships.js +10 -10
  29. package/dist/tests/templates/entities/types/array-template.spec.js +45 -45
  30. package/dist/tests/templates/entities/types/bigint-template.spec.js +43 -43
  31. package/dist/tests/templates/entities/types/boolean-template.spec.js +44 -44
  32. package/dist/tests/templates/entities/types/bytea-template.spec.js +35 -35
  33. package/dist/tests/templates/entities/types/char-template.spec.js +35 -35
  34. package/dist/tests/templates/entities/types/date-template.spec.js +29 -29
  35. package/dist/tests/templates/entities/types/decimal-field-validation.spec.js +46 -46
  36. package/dist/tests/templates/entities/types/decimal-template.spec.js +46 -46
  37. package/dist/tests/templates/entities/types/double-template.spec.js +37 -34
  38. package/dist/tests/templates/entities/types/enum-template.spec.js +42 -42
  39. package/dist/tests/templates/entities/types/float-template.spec.js +37 -34
  40. package/dist/tests/templates/entities/types/geometry-template.spec.js +31 -31
  41. package/dist/tests/templates/entities/types/inet-template.spec.js +36 -36
  42. package/dist/tests/templates/entities/types/int4range-template.spec.js +45 -33
  43. package/dist/tests/templates/entities/types/integer-template.spec.js +40 -40
  44. package/dist/tests/templates/entities/types/interval-template.spec.js +35 -35
  45. package/dist/tests/templates/entities/types/json-plain-template.spec.js +27 -27
  46. package/dist/tests/templates/entities/types/json-template.spec.js +19 -19
  47. package/dist/tests/templates/entities/types/jsonb-template.spec.js +19 -19
  48. package/dist/tests/templates/entities/types/money-template.spec.js +35 -35
  49. package/dist/tests/templates/entities/types/numeric-template.spec.js +44 -44
  50. package/dist/tests/templates/entities/types/point-template.spec.js +37 -31
  51. package/dist/tests/templates/entities/types/polygon-template.spec.js +43 -31
  52. package/dist/tests/templates/entities/types/real-template.spec.js +34 -34
  53. package/dist/tests/templates/entities/types/serial-template.spec.js +62 -62
  54. package/dist/tests/templates/entities/types/smallint-template.spec.js +43 -43
  55. package/dist/tests/templates/entities/types/string-template.spec.js +59 -59
  56. package/dist/tests/templates/entities/types/text-template.spec.js +37 -37
  57. package/dist/tests/templates/entities/types/time-template.spec.js +35 -35
  58. package/dist/tests/templates/entities/types/timestamp-template.spec.js +39 -39
  59. package/dist/tests/templates/entities/types/timetz-template.spec.js +35 -35
  60. package/dist/tests/templates/entities/types/tsvector-template.spec.js +28 -28
  61. package/dist/tests/templates/entities/types/uuid-template.spec.js +43 -43
  62. package/dist/tests/templates/entities/types/varchar-template.spec.js +59 -56
  63. package/dist/tests/templates/entities/types/xml-template.spec.js +28 -28
  64. package/dist/tests/templates/entities/utils/template-test-utils.d.ts +1 -1
  65. package/dist/tests/templates/entities/utils/template-test-utils.js +3 -3
  66. package/dist/tests/templates/entities/uuid-entity-template.spec.js +138 -138
  67. package/dist/tests/utils/relationship-deduplication.spec.js +11 -11
  68. package/oclif.manifest.json +1 -1
  69. package/package.json +1 -1
package/README.md CHANGED
@@ -35,7 +35,9 @@ npm run start:dev
35
35
  - [Populating an .apsorc File](#populating-an-apsorc-file)
36
36
  - [Auto-Generated Code Reference](#auto-generated-code-reference)
37
37
  - [Relationships](#relationships)
38
+ - [Authentication (Bring Your Own Auth)](#authentication-bring-your-own-auth)
38
39
  - [Data Scoping (Multi-Tenant Isolation)](#data-scoping-multi-tenant-isolation)
40
+ - [Authentication + Scoping: Working Together](#authentication--scoping-working-together)
39
41
  - [Schema Reference](#schema-reference)
40
42
  - [Debugging](#debugging)
41
43
  - [Commands](#commands)
@@ -746,9 +748,354 @@ Refer to the [example v2 file](#example-apsorc-v2-file) for usage.
746
748
  > - Avoid deep nesting and duplicate definitions.
747
749
  > - Always inspect generated code and test your build after scaffolding.
748
750
 
751
+ ## Authentication (Bring Your Own Auth)
752
+
753
+ Apso provides flexible, provider-agnostic authentication that generates NestJS guards from your `.apsorc` configuration. Unlike monolithic platforms that force vendor lock-in through proprietary auth systems, Apso embraces **code ownership** - you choose your auth provider, and you own the generated code.
754
+
755
+ ### Philosophy: Why Bring Your Own Auth Matters
756
+
757
+ Traditional BaaS platforms like Supabase and Firebase provide authentication as a core feature, but this creates dependency:
758
+ - Your user data lives in their systems
759
+ - Migrating away requires rewriting auth logic
760
+ - You're bound to their pricing, features, and roadmap
761
+
762
+ **Apso takes a different approach:**
763
+ - **Provider flexibility** - Use Better Auth, Auth0, Cognito, Clerk, or custom solutions
764
+ - **Code ownership** - Generated guards are standard NestJS code you can inspect, modify, and extend
765
+ - **Zero lock-in** - Switch providers by changing configuration, not rewriting code
766
+ - **Normalized interface** - All providers produce the same `AuthContext` consumed by scoping and RBAC
767
+
768
+ This philosophy ensures your authentication layer is **timeless** - it grows with your needs and migrates with your stack.
769
+
770
+ ### Supported Authentication Providers
771
+
772
+ | Provider | Type | Best For |
773
+ |----------|------|----------|
774
+ | `better-auth` | Database Sessions | Self-hosted apps, maximum control |
775
+ | `custom-db-session` | Database Sessions | Existing session tables, custom flows |
776
+ | `auth0` | JWT | Enterprise SSO, social login |
777
+ | `clerk` | JWT | Modern SaaS with prebuilt UI |
778
+ | `cognito` | JWT | AWS ecosystem integration |
779
+ | `api-key` | API Keys | Service-to-service auth, public APIs |
780
+
781
+ ### Basic Configuration
782
+
783
+ Add an `auth` block to your `.apsorc`:
784
+
785
+ ```json
786
+ {
787
+ "version": 2,
788
+ "auth": {
789
+ "provider": "better-auth"
790
+ },
791
+ "entities": [...]
792
+ }
793
+ ```
794
+
795
+ Run `apso server scaffold` to generate the auth guard.
796
+
797
+ ### Provider-Specific Configuration
798
+
799
+ #### Better Auth / Custom DB Sessions
800
+
801
+ For database-backed session authentication:
802
+
803
+ ```json
804
+ {
805
+ "auth": {
806
+ "provider": "better-auth",
807
+ "sessionEntity": "session",
808
+ "userEntity": "User",
809
+ "accountUserEntity": "AccountUser",
810
+ "cookiePrefix": "myapp",
811
+ "organizationField": "organizationId",
812
+ "roleField": "role"
813
+ }
814
+ }
815
+ ```
816
+
817
+ | Option | Default | Description |
818
+ |--------|---------|-------------|
819
+ | `sessionEntity` | `"session"` | Entity storing session tokens |
820
+ | `userEntity` | `"User"` | Entity for user accounts |
821
+ | `accountUserEntity` | `"AccountUser"` | Junction entity for user-org mapping |
822
+ | `cookiePrefix` | service name | Cookie name prefix (e.g., `myapp.session_token`) |
823
+ | `organizationField` | `"organizationId"` | Field on accountUserEntity for org ID |
824
+ | `roleField` | `"role"` | Field on accountUserEntity for user role |
825
+
826
+ #### JWT Providers (Auth0, Clerk, Cognito)
827
+
828
+ For JWT-based authentication:
829
+
830
+ ```json
831
+ {
832
+ "auth": {
833
+ "provider": "auth0",
834
+ "jwt": {
835
+ "issuer": "https://your-tenant.auth0.com/",
836
+ "audience": "https://your-api.example.com",
837
+ "jwksUri": "https://your-tenant.auth0.com/.well-known/jwks.json",
838
+ "algorithms": ["RS256"]
839
+ },
840
+ "claims": {
841
+ "userId": "sub",
842
+ "email": "email",
843
+ "organizationId": "org_id",
844
+ "roles": "permissions"
845
+ }
846
+ }
847
+ }
848
+ ```
849
+
850
+ | JWT Option | Default | Description |
851
+ |------------|---------|-------------|
852
+ | `issuer` | Required | JWT issuer URL |
853
+ | `audience` | Required | Expected JWT audience |
854
+ | `jwksUri` | `issuer + /.well-known/jwks.json` | JWKS endpoint for key rotation |
855
+ | `algorithms` | `["RS256"]` | Accepted signing algorithms |
856
+
857
+ | Claims Option | Default | Description |
858
+ |---------------|---------|-------------|
859
+ | `userId` | `"sub"` | Claim containing user ID |
860
+ | `email` | `"email"` | Claim containing user email |
861
+ | `organizationId` | - | Claim for org/workspace ID |
862
+ | `roles` | `"roles"` | Claim containing role array |
863
+
864
+ #### API Key Authentication
865
+
866
+ For service-to-service or public API authentication:
867
+
868
+ ```json
869
+ {
870
+ "auth": {
871
+ "provider": "api-key",
872
+ "apiKeyHeader": "x-api-key",
873
+ "apiKeyEntity": "ApiKey",
874
+ "workspaceField": "workspaceId",
875
+ "roleField": "permissions"
876
+ }
877
+ }
878
+ ```
879
+
880
+ | Option | Default | Description |
881
+ |--------|---------|-------------|
882
+ | `apiKeyHeader` | `"x-api-key"` | Header name for API key |
883
+ | `apiKeyEntity` | `"ApiKey"` | Entity storing API keys |
884
+ | `workspaceField` | - | Field on entity for workspace ID |
885
+ | `roleField` | - | Field on entity for permissions |
886
+
887
+ ### The AuthContext Interface
888
+
889
+ All auth providers produce a normalized `AuthContext` that's attached to every authenticated request:
890
+
891
+ ```typescript
892
+ interface AuthContext {
893
+ userId?: string; // The authenticated user's ID
894
+ email?: string; // User's email (if available)
895
+ workspaceId?: string; // Workspace/tenant ID
896
+ organizationId?: string; // Organization ID (alias)
897
+ roles: string[]; // User's roles/permissions
898
+ serviceId?: string; // For API key auth: the key identifier
899
+ user?: unknown; // Raw user object (provider-specific)
900
+ session?: unknown; // Raw session object (provider-specific)
901
+ }
902
+ ```
903
+
904
+ This normalization is powerful - your business logic works with the same interface regardless of whether you're using Auth0 JWTs or Better Auth sessions.
905
+
906
+ ### Generated Files
907
+
908
+ When `auth` is configured, `apso server scaffold` generates:
909
+
910
+ ```
911
+ src/
912
+ guards/
913
+ auth.guard.ts # Authentication guard implementation
914
+ scope.guard.ts # Data scoping guard (if scopeBy used)
915
+ guards.module.ts # NestJS module with providers
916
+ index.ts # Exports
917
+ ```
918
+
919
+ ### Enabling Authentication
920
+
921
+ Guards are generated but **not enabled globally by default**. Enable them based on your needs:
922
+
923
+ #### Option 1: Global Enable (Recommended for most apps)
924
+
925
+ Edit `src/guards/guards.module.ts`:
926
+
927
+ ```typescript
928
+ providers: [
929
+ AuthGuard,
930
+ ScopeGuard,
931
+ // Uncomment to enable globally:
932
+ {
933
+ provide: APP_GUARD,
934
+ useClass: AuthGuard,
935
+ },
936
+ {
937
+ provide: APP_GUARD,
938
+ useClass: ScopeGuard,
939
+ },
940
+ ],
941
+ ```
942
+
943
+ #### Option 2: Controller-Level Enable
944
+
945
+ ```typescript
946
+ import { AuthGuard } from '../guards';
947
+
948
+ @UseGuards(AuthGuard)
949
+ @Controller('projects')
950
+ export class ProjectController { }
951
+ ```
952
+
953
+ #### Option 3: Route-Level Enable
954
+
955
+ ```typescript
956
+ @UseGuards(AuthGuard)
957
+ @Get('me')
958
+ getProfile(@Req() req: AuthenticatedRequest) {
959
+ return req.auth;
960
+ }
961
+ ```
962
+
963
+ ### Decorators
964
+
965
+ ```typescript
966
+ import { Public, SkipScopeCheck } from './guards';
967
+
968
+ // Skip ALL guards (no authentication required)
969
+ @Public()
970
+ @Get('health')
971
+ healthCheck() { }
972
+
973
+ // Skip only scope checking (auth still required)
974
+ @SkipScopeCheck()
975
+ @Get('admin/stats')
976
+ adminStats() { }
977
+ ```
978
+
979
+ ### Accessing Auth Context
980
+
981
+ In controllers and services, access the authenticated context:
982
+
983
+ ```typescript
984
+ import { AuthenticatedRequest, getAuthContext, requireAuthContext } from './guards';
985
+
986
+ @Controller('projects')
987
+ export class ProjectController {
988
+ @Get()
989
+ findAll(@Req() req: AuthenticatedRequest) {
990
+ // Direct access
991
+ const userId = req.auth.userId;
992
+ const orgId = req.auth.organizationId;
993
+
994
+ // Or use helpers
995
+ const ctx = requireAuthContext(req); // Throws if not authenticated
996
+ return this.projectService.findByOrg(ctx.organizationId);
997
+ }
998
+ }
999
+ ```
1000
+
1001
+ ### Token Extraction
1002
+
1003
+ The generated auth guard extracts tokens from multiple locations (in order):
1004
+
1005
+ 1. **Authorization header**: `Bearer <token>`
1006
+ 2. **Cookies**: `{cookiePrefix}.session_token`, `better-auth.session_token`, or `session_token`
1007
+ 3. **Custom header**: `X-Session-Token`
1008
+
1009
+ This flexibility supports both browser-based apps (cookies) and API clients (headers).
1010
+
1011
+ ### Complete Example
1012
+
1013
+ ```json
1014
+ {
1015
+ "version": 2,
1016
+ "auth": {
1017
+ "provider": "better-auth",
1018
+ "sessionEntity": "session",
1019
+ "userEntity": "User",
1020
+ "accountUserEntity": "AccountUser",
1021
+ "cookiePrefix": "myapp",
1022
+ "organizationField": "organizationId",
1023
+ "roleField": "role"
1024
+ },
1025
+ "entities": [
1026
+ {
1027
+ "name": "User",
1028
+ "fields": [
1029
+ { "name": "email", "type": "text", "unique": true },
1030
+ { "name": "name", "type": "text", "nullable": true }
1031
+ ]
1032
+ },
1033
+ {
1034
+ "name": "session",
1035
+ "fields": [
1036
+ { "name": "token", "type": "text", "unique": true },
1037
+ { "name": "expiresAt", "type": "timestamp" },
1038
+ { "name": "userId", "type": "text" }
1039
+ ]
1040
+ },
1041
+ {
1042
+ "name": "Organization",
1043
+ "fields": [
1044
+ { "name": "name", "type": "text" }
1045
+ ]
1046
+ },
1047
+ {
1048
+ "name": "AccountUser",
1049
+ "fields": [
1050
+ { "name": "role", "type": "enum", "values": ["owner", "admin", "member"] }
1051
+ ]
1052
+ },
1053
+ {
1054
+ "name": "Project",
1055
+ "scopeBy": "organizationId",
1056
+ "fields": [
1057
+ { "name": "name", "type": "text" }
1058
+ ]
1059
+ }
1060
+ ],
1061
+ "relationships": [
1062
+ { "from": "AccountUser", "to": "User", "type": "ManyToOne" },
1063
+ { "from": "AccountUser", "to": "Organization", "type": "ManyToOne" },
1064
+ { "from": "Project", "to": "Organization", "type": "ManyToOne" }
1065
+ ]
1066
+ }
1067
+ ```
1068
+
1069
+ ### Migrating Between Providers
1070
+
1071
+ One of Apso's key advantages is seamless provider migration:
1072
+
1073
+ 1. Update the `auth` block in `.apsorc`
1074
+ 2. Run `apso server scaffold`
1075
+ 3. Update your frontend to use the new provider's login flow
1076
+
1077
+ Your business logic remains unchanged because it only interacts with the normalized `AuthContext`.
1078
+
1079
+ ---
1080
+
749
1081
  ## Data Scoping (Multi-Tenant Isolation)
750
1082
 
751
- Apso CLI supports automatic generation of scope guards that enforce data isolation at the application layer. This is useful for multi-tenant applications where users should only access data within their assigned scope (e.g., workspace, organization, team).
1083
+ Apso provides application-layer data isolation that rivals PostgreSQL's Row-Level Security (RLS) - but with greater flexibility, visibility, and portability. This is Apso's answer to one of the most critical challenges in SaaS development: ensuring users only see and modify data they're authorized to access.
1084
+
1085
+ ### Philosophy: Application-Layer RLS
1086
+
1087
+ Traditional approaches to multi-tenant data isolation include:
1088
+ - **Database RLS (Supabase)** - Powerful but opaque, tied to PostgreSQL, difficult to debug
1089
+ - **Manual filtering** - Error-prone, repetitive, easy to forget on new endpoints
1090
+ - **ORM middleware** - Often complex, hard to customize
1091
+
1092
+ **Apso's approach delivers the best of all worlds:**
1093
+ - **Declarative** - Define scope once in `.apsorc`, applied everywhere
1094
+ - **Transparent** - Generated guards are standard NestJS code you can inspect and debug
1095
+ - **Portable** - Works with any database, not locked to PostgreSQL RLS
1096
+ - **Flexible** - Configure per-entity behavior: auto-injection, filtering, bypass rules
1097
+
1098
+ This design is **timeless** - your data isolation logic is explicit code, not hidden database magic.
752
1099
 
753
1100
  ### What is scopeBy?
754
1101
 
@@ -961,6 +1308,168 @@ These are intentionally separate. Use `scopeBy` for data isolation, and implemen
961
1308
 
962
1309
  ---
963
1310
 
1311
+ ## Authentication + Scoping: Working Together
1312
+
1313
+ Auth and Scoping are designed to work together seamlessly. This combined system delivers enterprise-grade security with minimal configuration.
1314
+
1315
+ ### How They Connect
1316
+
1317
+ When both `auth` and `scopeBy` are configured:
1318
+
1319
+ 1. **AuthGuard runs first** - Validates the session/token and populates `request.auth`
1320
+ 2. **ScopeGuard runs second** - Reads `organizationId`/`workspaceId` from `request.auth` and enforces isolation
1321
+
1322
+ The `AuthContext` automatically provides the scope values that `scopeBy` needs:
1323
+
1324
+ ```typescript
1325
+ // AuthGuard sets this on every authenticated request:
1326
+ request.auth = {
1327
+ userId: "user_123",
1328
+ organizationId: "org_456", // <-- ScopeGuard uses this
1329
+ workspaceId: "org_456", // <-- Or this (alias)
1330
+ roles: ["admin"],
1331
+ // ...
1332
+ }
1333
+
1334
+ // ScopeGuard then uses organizationId to:
1335
+ // - Filter GET /projects -> only org_456's projects
1336
+ // - Inject on POST /projects -> auto-set organizationId
1337
+ // - Verify on GET /projects/:id -> ensure it belongs to org_456
1338
+ ```
1339
+
1340
+ ### Complete Multi-Tenant Example
1341
+
1342
+ ```json
1343
+ {
1344
+ "version": 2,
1345
+ "auth": {
1346
+ "provider": "better-auth",
1347
+ "sessionEntity": "session",
1348
+ "userEntity": "User",
1349
+ "accountUserEntity": "AccountUser",
1350
+ "organizationField": "organizationId"
1351
+ },
1352
+ "entities": [
1353
+ {
1354
+ "name": "User",
1355
+ "fields": [
1356
+ { "name": "email", "type": "text", "unique": true },
1357
+ { "name": "name", "type": "text", "nullable": true }
1358
+ ]
1359
+ },
1360
+ {
1361
+ "name": "session",
1362
+ "fields": [
1363
+ { "name": "token", "type": "text", "unique": true },
1364
+ { "name": "expiresAt", "type": "timestamp" },
1365
+ { "name": "userId", "type": "text" }
1366
+ ]
1367
+ },
1368
+ {
1369
+ "name": "Organization",
1370
+ "fields": [
1371
+ { "name": "name", "type": "text" },
1372
+ { "name": "plan", "type": "enum", "values": ["free", "pro", "enterprise"] }
1373
+ ]
1374
+ },
1375
+ {
1376
+ "name": "AccountUser",
1377
+ "fields": [
1378
+ { "name": "role", "type": "enum", "values": ["owner", "admin", "member"] }
1379
+ ]
1380
+ },
1381
+ {
1382
+ "name": "Project",
1383
+ "scopeBy": "organizationId",
1384
+ "fields": [
1385
+ { "name": "name", "type": "text" },
1386
+ { "name": "status", "type": "enum", "values": ["active", "archived"] }
1387
+ ]
1388
+ },
1389
+ {
1390
+ "name": "Task",
1391
+ "scopeBy": ["organizationId", "projectId"],
1392
+ "fields": [
1393
+ { "name": "title", "type": "text" },
1394
+ { "name": "completed", "type": "boolean", "default": false }
1395
+ ]
1396
+ },
1397
+ {
1398
+ "name": "AuditLog",
1399
+ "scopeBy": "organizationId",
1400
+ "scopeOptions": {
1401
+ "injectOnCreate": true,
1402
+ "enforceOn": ["find", "get"],
1403
+ "bypassRoles": ["superadmin"]
1404
+ },
1405
+ "fields": [
1406
+ { "name": "action", "type": "text" },
1407
+ { "name": "details", "type": "json" }
1408
+ ]
1409
+ }
1410
+ ],
1411
+ "relationships": [
1412
+ { "from": "AccountUser", "to": "User", "type": "ManyToOne" },
1413
+ { "from": "AccountUser", "to": "Organization", "type": "ManyToOne" },
1414
+ { "from": "Project", "to": "Organization", "type": "ManyToOne" },
1415
+ { "from": "Task", "to": "Project", "type": "ManyToOne" },
1416
+ { "from": "Task", "to": "Organization", "type": "ManyToOne" },
1417
+ { "from": "AuditLog", "to": "Organization", "type": "ManyToOne" }
1418
+ ]
1419
+ }
1420
+ ```
1421
+
1422
+ ### Guard Execution Order
1423
+
1424
+ Enable both guards globally for automatic protection:
1425
+
1426
+ ```typescript
1427
+ // src/guards/guards.module.ts
1428
+ providers: [
1429
+ AuthGuard,
1430
+ ScopeGuard,
1431
+ {
1432
+ provide: APP_GUARD,
1433
+ useClass: AuthGuard, // Runs first
1434
+ },
1435
+ {
1436
+ provide: APP_GUARD,
1437
+ useClass: ScopeGuard, // Runs second
1438
+ },
1439
+ ],
1440
+ ```
1441
+
1442
+ ### The Apso Security Stack
1443
+
1444
+ | Layer | Guard | Question Answered | Configuration |
1445
+ |-------|-------|-------------------|---------------|
1446
+ | 1. Identity | AuthGuard | "Who is this user?" | `auth` in `.apsorc` |
1447
+ | 2. Isolation | ScopeGuard | "Which data can they see?" | `scopeBy` on entities |
1448
+ | 3. Authorization | (Your implementation) | "What actions can they take?" | Custom RBAC guard |
1449
+
1450
+ Apso handles layers 1 and 2 automatically. Layer 3 (fine-grained permissions like "can edit this specific resource") is left to your business logic since it varies widely between applications.
1451
+
1452
+ ### Why This Matters: The Supabase Comparison
1453
+
1454
+ | Feature | Supabase | Apso |
1455
+ |---------|----------|------|
1456
+ | **Auth** | Built-in, proprietary | Bring your own, code you own |
1457
+ | **Data Isolation** | PostgreSQL RLS (opaque) | Application-layer guards (transparent) |
1458
+ | **Portability** | Locked to Supabase | Works with any database |
1459
+ | **Debugging** | Database logs, hard to trace | Standard NestJS code, full visibility |
1460
+ | **Customization** | Limited to RLS policies | Unlimited - it's your code |
1461
+ | **Migration Path** | Rewrite required | Change config, regenerate |
1462
+
1463
+ Apso delivers equivalent functionality to Supabase's auth + RLS combo, but with:
1464
+ - **Full code ownership** - No vendor lock-in
1465
+ - **Provider flexibility** - Auth0, Clerk, Cognito, or self-hosted
1466
+ - **Database freedom** - PostgreSQL, MySQL, MongoDB, or any TypeORM-supported database
1467
+ - **Complete transparency** - Debug with standard tools, not vendor-specific dashboards
1468
+
1469
+ This architecture is **timeless** - it grows with your needs, migrates with your stack, and remains fully under your control.
1470
+
1471
+ ---
1472
+
964
1473
  ## Schema Reference
965
1474
 
966
1475
  The `.apsorc` file must conform to the [APSO Configuration Schema](./apsorc.schema.json). This schema defines all valid properties, types, and constraints for your configuration file.
@@ -72,7 +72,7 @@ class New extends base_command_1.default {
72
72
  let projectName = flags.name;
73
73
  let apiType = (_a = flags.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
74
74
  if (!projectName) {
75
- projectName = '';
75
+ projectName = "";
76
76
  }
77
77
  if (!apiType) {
78
78
  apiType = "rest";
@@ -10,7 +10,7 @@ const perf_hooks_1 = require("perf_hooks");
10
10
  const Eta = tslib_1.__importStar(require("eta"));
11
11
  Eta.configure({
12
12
  views: path.join(__dirname, "../../lib/templates"),
13
- cache: false
13
+ cache: false,
14
14
  });
15
15
  class Scaffold extends base_command_1.default {
16
16
  async scaffoldServer(options) {
@@ -25,7 +25,7 @@ class Scaffold extends base_command_1.default {
25
25
  entity,
26
26
  relationships: entityRelationships,
27
27
  apiType,
28
- allEntities
28
+ allEntities,
29
29
  });
30
30
  switch (apiType) {
31
31
  case apsorc_parser_1.ApiType.Graphql:
@@ -45,18 +45,25 @@ class Scaffold extends base_command_1.default {
45
45
  const filePath = path.join(dir, entity.name);
46
46
  const entityRelationships = relationshipMap[entity.name] || [];
47
47
  const tDto = perf_hooks_1.performance.now();
48
- await (0, lib_1.createDto)(filePath, entity, entityRelationships, { apiType, allEntities });
48
+ await (0, lib_1.createDto)(filePath, entity, entityRelationships, {
49
+ apiType,
50
+ allEntities,
51
+ });
49
52
  if (process.env.DEBUG) {
50
53
  console.log(`[timing] createDto for ${entity.name}: ${(perf_hooks_1.performance.now() - tDto).toFixed(2)}ms`);
51
54
  const used = process.memoryUsage();
52
- console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
55
+ console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed /
56
+ 1024 /
57
+ 1024).toFixed(2)} MB`);
53
58
  }
54
59
  const tService = perf_hooks_1.performance.now();
55
60
  await (0, lib_1.createService)(filePath, entity, relationshipMap);
56
61
  if (process.env.DEBUG) {
57
62
  console.log(`[timing] createService for ${entity.name}: ${(perf_hooks_1.performance.now() - tService).toFixed(2)}ms`);
58
63
  const used = process.memoryUsage();
59
- console.log(`[mem] heapUsed after createService for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
64
+ console.log(`[mem] heapUsed after createService for ${entity.name}: ${(used.heapUsed /
65
+ 1024 /
66
+ 1024).toFixed(2)} MB`);
60
67
  }
61
68
  const tController = perf_hooks_1.performance.now();
62
69
  await (0, lib_1.createController)(filePath, entity, relationshipMap);
@@ -71,7 +78,7 @@ class Scaffold extends base_command_1.default {
71
78
  }
72
79
  async run() {
73
80
  const totalBuildStart = perf_hooks_1.performance.now();
74
- const { rootFolder, entities, relationshipMap, apiType } = (0, lib_1.parseApsorc)();
81
+ const { rootFolder, entities, relationshipMap, apiType, auth } = (0, lib_1.parseApsorc)();
75
82
  const rootPath = path.join(process.cwd(), rootFolder);
76
83
  const autogenPath = path.join(rootPath, "autogen");
77
84
  const lowerCaseApiType = apiType.toLowerCase();
@@ -83,15 +90,17 @@ class Scaffold extends base_command_1.default {
83
90
  entity,
84
91
  relationshipMap,
85
92
  apiType: lowerCaseApiType,
86
- allEntities: entities
93
+ allEntities: entities,
87
94
  });
88
95
  if (process.env.DEBUG) {
89
96
  const used = process.memoryUsage();
90
- console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
97
+ console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed /
98
+ 1024 /
99
+ 1024).toFixed(2)} MB`);
91
100
  }
92
101
  }));
93
- // Generate scope guards if any entities have scopeBy configured
94
- await (0, lib_1.createGuards)(rootPath, entities);
102
+ // Generate guards (auth and/or scope) based on .apsorc configuration
103
+ await (0, lib_1.createGuards)(rootPath, entities, auth);
95
104
  await (0, lib_1.createIndexAppModule)(autogenPath, entities, lowerCaseApiType);
96
105
  const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
97
106
  console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
@@ -1,5 +1,6 @@
1
1
  import { Entity } from "./types/entity";
2
2
  import { ApsorcRelationship, RelationshipMap } from "./types/relationship";
3
+ import { AuthConfig } from "./types/auth";
3
4
  export declare enum ApiType {
4
5
  Graphql = "graphql",
5
6
  Rest = "rest"
@@ -10,6 +11,7 @@ export type ApsorcType = {
10
11
  entities: Entity[];
11
12
  apiType: ApiType;
12
13
  relationships: ApsorcRelationship[];
14
+ auth?: AuthConfig;
13
15
  };
14
16
  type ParsedApsorcData = {
15
17
  entities: Entity[];
@@ -20,6 +22,7 @@ type ParsedApsorc = {
20
22
  apiType: string;
21
23
  entities: Entity[];
22
24
  relationshipMap: RelationshipMap;
25
+ auth?: AuthConfig;
23
26
  };
24
27
  export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
25
28
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;