Skip to content

Amplify Gen 2: belongsTo forces inverse relations, exposing sensitive data (need support for one-way or hidden relations) #3334

@99electronic

Description

@99electronic

Environment information

System:
  OS: macOS 26.0
  CPU: (12) arm64 Apple M2 Max
  Memory: 1.37 GB / 96.00 GB
  Shell: /bin/zsh
Binaries:
  Node: 20.18.0 - ~/.nvm/versions/node/v20.18.0/bin/node
  Yarn: undefined - undefined
  npm: 11.4.0 - ~/.nvm/versions/node/v20.18.0/bin/npm
  pnpm: undefined - undefined
NPM Packages:
  @aws-amplify/auth-construct: 1.8.1
  @aws-amplify/backend: 1.16.1
  @aws-amplify/backend-ai: Not Found
  @aws-amplify/backend-auth: 1.7.1
  @aws-amplify/backend-cli: 1.8.0
  @aws-amplify/backend-data: 1.6.1
  @aws-amplify/backend-deployer: 2.1.3
  @aws-amplify/backend-function: 1.14.1
  @aws-amplify/backend-output-schemas: 1.7.0
  @aws-amplify/backend-output-storage: 1.3.1
  @aws-amplify/backend-secret: 1.4.0
  @aws-amplify/backend-storage: 1.4.1
  @aws-amplify/cli-core: 2.2.1
  @aws-amplify/client-config: 1.8.0
  @aws-amplify/data-construct: 1.16.3
  @aws-amplify/data-schema: 1.21.1
  @aws-amplify/deployed-backend-client: 1.8.0
  @aws-amplify/form-generator: 1.2.4
  @aws-amplify/model-generator: 1.2.0
  @aws-amplify/platform-core: 1.10.0
  @aws-amplify/plugin-types: 1.11.0
  @aws-amplify/sandbox: 2.1.2
  @aws-amplify/schema-generator: 1.4.0
  @aws-cdk/toolkit-lib: 1.1.1
  aws-amplify: 6.15.5
  aws-cdk-lib: 2.204.0
  typescript: 5.9.2
npm warn exec The following package was not found and will be installed: [email protected]
No AWS environment variables
No CDK environment variables

Data packages

[email protected] /Users/pavolkolencin/Documents/Coding/cedarcare/data-model
├─┬ @aws-amplify/[email protected]
│ └─┬ @aws-amplify/[email protected]
│   └── @aws-amplify/[email protected]
└─┬ @aws-amplify/[email protected]
  └─┬ @aws-amplify/[email protected]
    └── @aws-amplify/[email protected]

Description

Description
In Amplify Gen 1, it was possible to define one-way relationships safely.
For example:

type Patient @model {
  id: ID!
  clinicID: ID!
  clinic: Clinic! @hasOne(fields: ["clinicID"])
}

type Clinic @model {
  id: ID!
}

This allowed us to link a Patient → Clinic without automatically exposing Clinic → Patients.
This was critical for security and compliance, because sometimes the reverse edge leaks far too much data.
Problem in Gen 2
In Amplify Gen 2 with the TypeScript schema builder:

Patient: a.model({
  id: a.string().required(),
  clinicID: a.string().required(),
  clinic: a.belongsTo('Clinic', 'clinicID'),
})

Clinic: a.model({
  id: a.string().required(),
  // forced inverse relation
  patients: a.hasMany('Patient', 'clinicID'),
})

If you use belongsTo(), the compiler forces you to declare the inverse relation (hasMany).
This means Clinic will always expose a patients field, even if we don’t want clients to see it.

Unlike scalar/index fields, there is no .hidden() support for relation fields.
.authorization(() => []) doesn’t truly deny access — it inherits the parent model’s rules unless you explicitly block every group.
The result is that we are forced to expose clinic.patients, even in cases where that breaks our security model.

Why this is a Security Issue
In healthcare (HIPAA) and other sensitive domains, this is not just an annoyance:
We may want to allow Patient → Clinic (to know which clinic owns a patient) but never allow Clinic → Patients (to prevent enumeration of all patients in a clinic).

With Gen 2’s current behavior, the GraphQL schema always exposes the reverse list. Even if it resolves empty, the field itself is visible and queryable.
This leads to overexposure of data and complicates compliance audits.
In Gen 1, one-way modeling was possible and safe. In Gen 2, it is not.

Current Workarounds
Hack: define the inverse as hasOne instead of hasMany — the compiler is satisfied, but queries to it fail at runtime.
Manual FK modeling: avoid belongsTo entirely and just use a raw string foreign key (clinicID), resolving relations manually.
Custom resolver override: keep the inverse, but replace its resolver with one that always returns [].
All of these are fragile or add unnecessary boilerplate.

Suggested Solutions
Add support for .hidden() on relation fields, the same way we can hide scalars or index helpers.
Or provide an explicit API, e.g.:
a.belongsTo('Clinic', 'clinicID', { inverse: false })
Or a silent variant such as:
a.hasSilentMany('Patient', 'clinicID')
which satisfies schema validation but does not expose the field in GraphQL.

Summary
Gen 2 currently forces bidirectional modeling, which is a regression compared to Gen 1.
This leads to unnecessary data exposure and creates compliance risks.
We need a way to safely define one-way relations (Patient → Clinic) without exposing unwanted inverse fields (Clinic → Patients) in the schema.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions