Install
$ agentstack add skill-sabahattink-antigravity-fullstack-hq-software-architecture ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Software Architecture
Clean Architecture Layers
┌─────────────────────────────────────────────┐
│ Frameworks & Drivers │ ← NestJS, TypeORM, Express
├─────────────────────────────────────────────┤
│ Interface Adapters │ ← Controllers, Repositories, Presenters
├─────────────────────────────────────────────┤
│ Application Layer │ ← Use Cases, Application Services
├─────────────────────────────────────────────┤
│ Domain Layer │ ← Entities, Value Objects, Domain Services
└─────────────────────────────────────────────┘
Dependency Rule: outer layers depend on inner layers — NEVER the reverse.
Domain Layer
Entities
// domain/user/user.entity.ts
// Entities contain identity and business rules. No framework dependencies.
export class User {
private constructor(
public readonly id: UserId,
public readonly email: Email,
private _name: string,
private _role: UserRole,
public readonly createdAt: Date,
) {}
static create(props: {
id: UserId
email: Email
name: string
role?: UserRole
}): User {
if (!props.name.trim()) {
throw new DomainError('Name cannot be empty')
}
return new User(
props.id,
props.email,
props.name.trim(),
props.role ?? UserRole.USER,
new Date(),
)
}
get name(): string { return this._name }
get role(): UserRole { return this._role }
rename(newName: string): User {
if (!newName.trim()) throw new DomainError('Name cannot be empty')
// Return new instance — immutability
return new User(this.id, this.email, newName.trim(), this._role, this.createdAt)
}
promote(to: UserRole, by: User): User {
if (by.role !== UserRole.ADMIN) {
throw new DomainError('Only admins can promote users')
}
return new User(this.id, this.email, this._name, to, this.createdAt)
}
}
Value Objects
// domain/shared/value-objects/email.vo.ts
export class Email {
private constructor(public readonly value: string) {}
static create(raw: string): Email {
const normalized = raw.toLowerCase().trim()
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
throw new DomainError(`Invalid email: ${raw}`)
}
return new Email(normalized)
}
equals(other: Email): boolean {
return this.value === other.value
}
toString(): string {
return this.value
}
}
// domain/shared/value-objects/money.vo.ts
export class Money {
private constructor(
public readonly amount: number, // in cents
public readonly currency: string,
) {}
static of(amount: number, currency: string): Money {
if (amount {
constructor(
private readonly userRepo: IUserRepository,
private readonly hasher: IPasswordHasher,
private readonly eventBus: EventBus,
) {}
async execute(cmd: CreateUserCommand): Promise {
const email = Email.create(cmd.email)
const exists = await this.userRepo.existsByEmail(email)
if (exists) throw new ConflictException('Email already in use')
const passwordHash = await this.hasher.hash(cmd.password)
const userId = UserId.generate()
const user = User.create({
id: userId,
email,
name: cmd.name,
passwordHash,
})
await this.userRepo.save(user)
this.eventBus.publish(new UserCreatedEvent(userId.value, email.value, cmd.name))
return userId
}
}
Repository Pattern (Interface in Domain)
// domain/user/user.repository.interface.ts
export interface IUserRepository {
findById(id: UserId): Promise
findByEmail(email: Email): Promise
existsByEmail(email: Email): Promise
save(user: User): Promise
delete(id: UserId): Promise
}
// infrastructure/persistence/typeorm/user.typeorm-repository.ts
@Injectable()
export class UserTypeOrmRepository implements IUserRepository {
constructor(
@InjectRepository(UserOrmEntity)
private readonly ormRepo: Repository,
private readonly mapper: UserMapper,
) {}
async findById(id: UserId): Promise {
const row = await this.ormRepo.findOne({ where: { id: id.value } })
return row ? this.mapper.toDomain(row) : null
}
async save(user: User): Promise {
const row = this.mapper.toOrm(user)
await this.ormRepo.save(row)
}
// ...
}
SOLID in Practice
Single Responsibility
// Bad: UserService does too much
class UserService {
async register(dto) { /* creates user + sends email + logs audit */ }
async updateProfile(dto) { /* validates + updates + notifies */ }
async generateReport() { /* queries DB + formats CSV + sends email */ }
}
// Good: each class has one reason to change
class UserRegistrationService { /* only: validate, create, emit event */ }
class EmailNotificationService { /* only: send emails */ }
class AuditLogService { /* only: write audit entries */ }
class UserReportService { /* only: query, format, export */ }
Open/Closed
// Open for extension, closed for modification
interface NotificationChannel {
send(message: NotificationMessage): Promise
}
class EmailChannel implements NotificationChannel { /* ... */ }
class SmsChannel implements NotificationChannel { /* ... */ }
class SlackChannel implements NotificationChannel { /* ... */ }
class NotificationService {
constructor(private channels: NotificationChannel[]) {}
// No modification needed when adding a new channel
async notify(message: NotificationMessage) {
await Promise.all(this.channels.map(c => c.send(message)))
}
}
Dependency Inversion
// Domain doesn't depend on infrastructure
// Bad:
class OrderService {
private repo = new TypeOrmOrderRepository() // concrete dep!
}
// Good:
@Injectable()
class OrderService {
constructor(
@Inject(ORDER_REPOSITORY_TOKEN)
private readonly repo: IOrderRepository, // interface dep
) {}
}
Module Design
// users/users.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([UserOrmEntity]),
CqrsModule,
ConfigModule,
],
controllers: [UsersController],
providers: [
// Application
CreateUserHandler,
GetUserQueryHandler,
// Domain services
UserDomainService,
// Infrastructure adapters
{
provide: IUserRepository, // injection token
useClass: UserTypeOrmRepository,
},
{
provide: IPasswordHasher,
useClass: BcryptPasswordHasher,
},
],
exports: [IUserRepository], // only export what other modules need
})
export class UsersModule {}
Event-Driven Architecture
// Sagas coordinate cross-module workflows
@Injectable()
export class UserOnboardingSaga {
@Saga()
userCreated = (events$: Observable): Observable => {
return events$.pipe(
ofType(UserCreatedEvent),
map(event => new SendWelcomeEmailCommand(event.email, event.name)),
)
}
}
ADR (Architecture Decision Record) Template
# ADR-001: Use CQRS Pattern for Write-Heavy Modules
## Status
Accepted
## Context
Orders and inventory modules have complex write operations with multiple side effects.
Read and write models diverge significantly.
## Decision
Adopt CQRS using @nestjs/cqrs for these modules.
Simple CRUD modules (users, settings) remain using direct service calls.
## Consequences
+ Clear separation of read/write models
+ Easier to add event sourcing later
+ Better testability via command/query handlers
- Higher initial complexity
- Two data models to maintain in some cases
Forbidden Patterns
- Never have circular dependencies between modules — restructure into shared modules
- Never access the database from the domain layer — only through repository interfaces
- Never put I/O (HTTP, DB, file system) in domain entities or value objects
- Never use
staticmutable state in services — it breaks testability and concurrency - Never expose ORM entities directly to the API layer — map to DTOs
- Never put business rules in controllers — they belong in the domain or application layer
- Never use inheritance where composition would work — prefer interfaces and DI
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: sabahattink
- Source: sabahattink/antigravity-fullstack-hq
- License: MIT
- Homepage: https://github.com/sabahattink/antigravity-fullstack-hq
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.