# Visibility modifiers
**Author**: @wesb
<!-- Status of the document - draft, in-review, etc. - is conveyed via HackMD labels -->
[Github issue](https://github.com/TBD54566975/ftl/issues/1164)
## Description
Currently, all `verbs` within the cluster are visible without restrictions. We should establish a mechanism where `verbs` and `types` are private by default (only visible within the same module), but can be explicitly tagged with `export` to make them visible to other `modules`.
## Goals
- Allow developers to control visibility of `verbs` and `types`
## Non-Goals (optional)
- This is not RBAC (that will come later)
- Not implementing module `groups` but those might come in later, which would further limit the scope of `internal` to be within a module group
- Not implementing module `visibility` given that each verb must declare its `visibility`
## Design
Using the term `visibility` over `access` to allow for future RBAC updates when they are defined.
### Annotations
#### Private
`private` refers to `verbs` and `types` that are only visible with their `module`.
```go
//ftl:verb
//ftl:data
//ftl:enum
```
#### Exported
These will be visible to other modules within FTL
```go
//ftl:verb export
//ftl:data export
//ftl:enum export
```
#### Special cases
`//ftl:cron` is always private as it provides no benefit
being exposed to other modules
`//ftl:ingress` is always public as `ingress` provides no benefit to the outside world being private
#### Schema
```graphql
module one {
export data User {}
export verb one(one.Request) one.Reponse
verb another(one.Request) one.Request
}
module two {
export verb two(req two.Request) two.Response
+calls one.one # valid because `one.one` is `exported`.
}
module three {
export verb three(HttpRequest<[three.Request]>) HttpResponse<[three.Response], Empty>
+ingress http GET /three
+calls two.two # valid because `two.two` is exported.
}
```
#### Required changes
1. Add new `Visibility` enum and field to schema
2. Update parser for these new fields. Update all usages of `ftl:export` to `ftl:internal`. We'll need to go through other repos like `ftl-examples` and customer repos as well. Kotlin `@Export` needs to be converted to `@Internal`.
3. Update `ftl.Call`/`controller` logic to enforce `visibility` rules
4. Update external module code generator to not generate `decls` based on `visibility`
5. Console updates to enforce these rules as well
## Questions
- [x] What happens with `ingress` and `visibility private`. Does `ingress override`? Do we `error`?
- With the new `ftl:internal` style syntax, all verbs will explicity specify `visibility` and `ingress` will go away in favor of `ftl:public http GET /path` style syntax
- [x] Should we be explicit about visibility? Meaning, should we always include `visibility` for each verb in the schema even though it's defined at the module level?
- Yes, see above
- [x] Should we omit non-visible types during code gen
- Yes
- [x] What are the defaults if not provided (private?)
- There are no defaults `visibility` must be declared for all `verbs`
## Rejected Alternatives (optional)
#### Groups
`groups` act as visibility boundaries within the system, categorizing modules into distinct collections that share specific visibility rules. Each module can belong to multiple groups.
#### Visibility levels
- `public`: Accessible from anywhere, including ingress or cli
- `internal`: Accessible only from other modules and cli.
- `private`: Only accessible within the same module.
There is no default and every verb must declare its `visibility` or it won't actually be a verb.
#### Schema
```graphql
module one {
internal verb one(one.Request) one.Reponse
}
module two {
internal verb two(req two.Request) two.Response
+calls one.one # valid because `one.one` is `internal`.
}
module three {
public verb three(HttpRequest<[three.Request]>) HttpResponse<[three.Response], Empty>
+ingress http GET /three
+calls two.two # valid because `two.two` is internal.
}
```
#### Go
```go
package one
//ftl:internal
func One(ctx context.Context, req OneRequest) (OneResponse, error) {}
package two
//ftl:internal
func Two(ctx context.Context, req TwoRequest) (TwoResponse, error) {
resp, err := ftl.Call(ctx, one.One,...
}
package three
//ftl:public http GET /users/{userId}/posts/{postId}
func Three(ctx context.Context, req builtin.HttpRequest[ThreeRequest]) (builtin.HttpResponse[ThreeResponse, string], error) {
resp, err := ftl.Call(ctx, two.Two,...
}
```
```graphql
module one {
visibility protected
groups a, b
verb one(req one.Request) one.Response
+visibility protected # This verb is protected, visible only within the groups it belongs to
}
module two {
visibility private
groups a, c
verb two(req two.Request) two.Response
+calls one.one # Calls 'one.one' verb from 'module one', valid because both share group 'a'
+visibility protected # Overrides module visibility
}
module three {
visibility public
groups c
verb three(three.Request) three.Response
+ingress http GET /three
+calls two.two # Calls 'two.two' which is protected in 'module two', valid because both share group 'c'
+visibility public # This verb is publicly visible
}
```