Subject: [PATCH links] Add additional changes to help filter out spam accounts.

From: Peter Sanchez <[email-masked]>
Date: Wed, 20 Aug 2025 16:18:07 -0600

Changelog-updated: More changes to combat link spam farms.
---
 admin/routes.go                               |   2 +
 api/api_test.go                               | 196 ++++++++++++++++++
 api/graph/generated.go                        |  79 ++++++-
 api/graph/model/models_gen.go                 |   3 +-
 api/graph/schema.graphqls                     |   1 +
 api/graph/schema.resolvers.go                 |  39 +++-
 billing/processors.go                         |   4 +
 cmd/migrations.go                             |   7 +
 ...008_change_org_default_visibility.down.sql |   2 +
 .../0008_change_org_default_visibility.up.sql |   8 +
 models/organization.go                        |  21 +-
 models/schema.sql                             |   2 +-
 templates/admin_org_detail.html               |   2 +-
 13 files changed, 343 insertions(+), 23 deletions(-)
 create mode 100644 migrations/0008_change_org_default_visibility.down.sql
 create mode 100644 migrations/0008_change_org_default_visibility.up.sql

diff --git a/admin/routes.go b/admin/routes.go
index 8d42781..cb2e926 100644
--- a/admin/routes.go
+++ b/admin/routes.go
@@ -394,6 +394,7 @@ func (s *Service) OrgDetail(c echo.Context) error {
 	pd.Data["no_domains"] = lt.Translate("No Domains")
 	pd.Data["update_type"] = lt.Translate("Update Type")
 	pd.Data["visibility"] = lt.Translate("Visibility")
+	pd.Data["forced"] = lt.Translate("FORCED")
 
 	type GraphQLResponse struct {
 		Org *models.Organization `json:"getOrganization"`
@@ -412,6 +413,7 @@ func (s *Service) OrgDetail(c echo.Context) error {
 					billing {
 						status
 					}
+					visibilityOverride
 				}
 		}
 	}`)
diff --git a/api/api_test.go b/api/api_test.go
index 4e41f83..ee0bf90 100644
--- a/api/api_test.go
+++ b/api/api_test.go
@@ -2,6 +2,8 @@ package main_test
 
 import (
 	"context"
+	"database/sql"
+	"fmt"
 	"links"
 	"links/cmd"
 	"links/cmd/test"
@@ -2358,6 +2360,200 @@ func TestAPI(t *testing.T) {
 		c.Equal("gqlclient: server failure: Link Short Not Found", err.Error())
 	})
 
+	t.Run("recent links visibility filter", func(t *testing.T) {
+		user := test.NewTestUser(1, false, false, true, true)
+		authCtx := auth.Context(ctx, user)
+
+		var orgHidden, orgForced, orgPublic, orgPersonal models.Organization
+		err := database.WithTx(dbCtx, nil, func(tx *sql.Tx) error {
+			// Hidden org that should auto-promote after 4 links
+			orgHidden = models.Organization{
+				OwnerID:    int(user.ID),
+				OrgType:    models.OrgTypeUser,
+				Name:       "Hidden Org",
+				Slug:       "hidden-org",
+				IsActive:   true,
+				Visibility: models.VisibilityHidden,
+				Settings: models.OrganizationSettings{
+					Billing: models.BillingSettings{
+						Status: models.BillingStatusFree,
+					},
+				},
+			}
+			if err := orgHidden.Store(dbCtx); err != nil {
+				return err
+			}
+
+			// Forced hidden org that should stay hidden even with 5+ links
+			orgForced = models.Organization{
+				OwnerID:    int(user.ID),
+				OrgType:    models.OrgTypeUser,
+				Name:       "Forced Hidden Org",
+				Slug:       "forced-hidden-org",
+				IsActive:   true,
+				Visibility: models.VisibilityHidden,
+				Settings: models.OrganizationSettings{
+					VisibilityOverride: "FORCED",
+					Billing: models.BillingSettings{
+						Status: models.BillingStatusFree,
+					},
+				},
+			}
+			if err := orgForced.Store(dbCtx); err != nil {
+				return err
+			}
+
+			orgPublic = models.Organization{
+				OwnerID:    int(user.ID),
+				OrgType:    models.OrgTypeUser,
+				Name:       "Public Free Org",
+				Slug:       "public-free-org",
+				IsActive:   true,
+				Visibility: models.VisibilityPublic,
+				Settings: models.OrganizationSettings{
+					Billing: models.BillingSettings{
+						Status: models.BillingStatusFree,
+					},
+				},
+			}
+			if err := orgPublic.Store(dbCtx); err != nil {
+				return err
+			}
+
+			orgPersonal = models.Organization{
+				OwnerID:    int(user.ID),
+				OrgType:    models.OrgTypeUser,
+				Name:       "Personal Org Recent",
+				Slug:       "personal-org-recent",
+				IsActive:   true,
+				Visibility: models.VisibilityPublic,
+				Settings: models.OrganizationSettings{
+					Billing: models.BillingSettings{
+						Status: models.BillingStatusPersonal,
+					},
+				},
+			}
+			if err := orgPersonal.Store(dbCtx); err != nil {
+				return err
+			}
+
+			for i := 0; i < 2; i++ {
+				baseURL := models.BaseURL{
+					URL:        fmt.Sprintf("https://hidden.com/%d", i),
+					Visibility: models.VisibilityPublic,
+				}
+				if err := baseURL.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store hidden base URL %d: %w", i, err)
+				}
+				
+				link := models.OrgLink{
+					Title:      fmt.Sprintf("Hidden Link %d", i),
+					URL:        fmt.Sprintf("https://hidden.com/%d", i),
+					BaseURLID:  baseURL.ID,
+					OrgID:      orgHidden.ID,
+					UserID:     int(user.ID),
+					Visibility: models.OrgLinkVisibilityPublic,
+				}
+				if err := link.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store hidden link %d: %w", i, err)
+				}
+			}
+
+			// Add 5 links to forced hidden org - it should still stay hidden
+			for i := 0; i < 5; i++ {
+				baseURL := models.BaseURL{
+					URL:        fmt.Sprintf("https://forced.com/%d", i),
+					Visibility: models.VisibilityPublic,
+				}
+				if err := baseURL.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store forced base URL %d: %w", i, err)
+				}
+				
+				link := models.OrgLink{
+					Title:      fmt.Sprintf("Forced Link %d", i),
+					URL:        fmt.Sprintf("https://forced.com/%d", i),
+					BaseURLID:  baseURL.ID,
+					OrgID:      orgForced.ID,
+					UserID:     int(user.ID),
+					Visibility: models.OrgLinkVisibilityPublic,
+				}
+				if err := link.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store forced link %d: %w", i, err)
+				}
+			}
+
+			for i := 0; i < 5; i++ {
+				baseURL := models.BaseURL{
+					URL:        fmt.Sprintf("https://public.com/%d", i),
+					Visibility: models.VisibilityPublic,
+				}
+				if err := baseURL.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store public base URL %d: %w", i, err)
+				}
+				
+				link := models.OrgLink{
+					Title:      fmt.Sprintf("Public Link %d", i),
+					URL:        fmt.Sprintf("https://public.com/%d", i),
+					BaseURLID:  baseURL.ID,
+					OrgID:      orgPublic.ID,
+					UserID:     int(user.ID),
+					Visibility: models.OrgLinkVisibilityPublic,
+				}
+				if err := link.Store(dbCtx); err != nil {
+					return fmt.Errorf("failed to store public link %d: %w", i, err)
+				}
+			}
+
+			baseURL := models.BaseURL{
+				URL:        "https://personal.com",
+				Visibility: models.VisibilityPublic,
+			}
+			if err := baseURL.Store(dbCtx); err != nil {
+				return fmt.Errorf("failed to store personal base URL: %w", err)
+			}
+			
+			link := models.OrgLink{
+				Title:      "Personal Link",
+				URL:        "https://personal.com",
+				BaseURLID:  baseURL.ID,
+				OrgID:      orgPersonal.ID,
+				UserID:     int(user.ID),
+				Visibility: models.OrgLinkVisibilityPublic,
+			}
+			return link.Store(dbCtx)
+		})
+		c.NoError(err)
+
+		type GraphQLResponse struct {
+			OrgLinks struct {
+				Result []models.OrgLink `json:"result"`
+			} `json:"getOrgLinks"`
+		}
+		var result GraphQLResponse
+		q := `query GetRecentLinks {
+			getOrgLinks(input: {}) {
+				result {
+					id
+					title
+					orgSlug
+				}
+			}
+		}`
+		op := gqlclient.NewOperation(q)
+		err = links.Execute(authCtx, op, &result)
+		c.NoError(err)
+
+		orgSlugs := make(map[string]bool)
+		for _, link := range result.OrgLinks.Result {
+			orgSlugs[link.OrgSlug] = true
+		}
+
+		c.False(orgSlugs["hidden-org"], "HIDDEN org should be filtered")
+		c.False(orgSlugs["forced-hidden-org"], "FORCED HIDDEN org should be filtered even with 5+ links")
+		c.True(orgSlugs["public-free-org"], "PUBLIC org should appear")
+		c.True(orgSlugs["personal-org-recent"], "Non-FREE PUBLIC org should always appear")
+	})
+
 	t.Run("list listings denied", func(t *testing.T) {
 		ctx := server.ServerContext(context.Background(), srv)
 		ctx = auth.Context(ctx, test.NewTestUser(2, false, false, true, true))
diff --git a/api/graph/generated.go b/api/graph/generated.go
index 5acb2f3..94c875b 100644
--- a/api/graph/generated.go
+++ b/api/graph/generated.go
@@ -337,8 +337,9 @@ type ComplexityRoot struct {
 	}
 
 	OrganizationSettings struct {
-		Billing     func(childComplexity int) int
-		DefaultPerm func(childComplexity int) int
+		Billing            func(childComplexity int) int
+		DefaultPerm        func(childComplexity int) int
+		VisibilityOverride func(childComplexity int) int
 	}
 
 	OrganizationStats struct {
@@ -2104,6 +2105,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
 
 		return e.complexity.OrganizationSettings.DefaultPerm(childComplexity), true
 
+	case "OrganizationSettings.visibilityOverride":
+		if e.complexity.OrganizationSettings.VisibilityOverride == nil {
+			break
+		}
+
+		return e.complexity.OrganizationSettings.VisibilityOverride(childComplexity), true
+
 	case "OrganizationStats.links":
 		if e.complexity.OrganizationStats.Links == nil {
 			break
@@ -15292,6 +15300,8 @@ func (ec *executionContext) fieldContext_Organization_settings(_ context.Context
 				return ec.fieldContext_OrganizationSettings_defaultPerm(ctx, field)
 			case "billing":
 				return ec.fieldContext_OrganizationSettings_billing(ctx, field)
+			case "visibilityOverride":
+				return ec.fieldContext_OrganizationSettings_visibilityOverride(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type OrganizationSettings", field.Name)
 		},
@@ -15793,6 +15803,69 @@ func (ec *executionContext) fieldContext_OrganizationSettings_billing(_ context.
 	return fc, nil
 }
 
+func (ec *executionContext) _OrganizationSettings_visibilityOverride(ctx context.Context, field graphql.CollectedField, obj *models.OrganizationSettings) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_OrganizationSettings_visibilityOverride(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+		directive0 := func(rctx context.Context) (any, error) {
+			ctx = rctx // use context from middleware stack in children
+			return obj.VisibilityOverride, nil
+		}
+
+		directive1 := func(ctx context.Context) (any, error) {
+			if ec.directives.Admin == nil {
+				var zeroVal string
+				return zeroVal, errors.New("directive admin is not implemented")
+			}
+			return ec.directives.Admin(ctx, obj, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(string); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalOString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_OrganizationSettings_visibilityOverride(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "OrganizationSettings",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _OrganizationStats_links(ctx context.Context, field graphql.CollectedField, obj *model.OrganizationStats) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_OrganizationStats_links(ctx, field)
 	if err != nil {
@@ -28728,6 +28801,8 @@ func (ec *executionContext) _OrganizationSettings(ctx context.Context, sel ast.S
 			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
 		case "billing":
 			out.Values[i] = ec._OrganizationSettings_billing(ctx, field, obj)
+		case "visibilityOverride":
+			out.Values[i] = ec._OrganizationSettings_visibilityOverride(ctx, field, obj)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go
index ac45cf5..afc1aff 100644
--- a/api/graph/model/models_gen.go
+++ b/api/graph/model/models_gen.go
@@ -435,7 +435,8 @@ type TagInput struct {
 	OrgSlug string        `json:"orgSlug"`
 	Service DomainService `json:"service"`
 	Tag     string        `json:"tag"`
-	NewTag  *string       `json:"newTag,omitempty"`
+	// newTag is required for `renameTag` mutation
+	NewTag *string `json:"newTag,omitempty"`
 }
 
 type UpdateAdminDomainInput struct {
diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls
index 4799141..273d2aa 100644
--- a/api/graph/schema.graphqls
+++ b/api/graph/schema.graphqls
@@ -171,6 +171,7 @@ type BillingSettings {
 type OrganizationSettings {
     defaultPerm: LinkVisibility!
     billing: BillingSettings
+    visibilityOverride: String @admin
 }
 
 type Organization {
diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index 220803d..e05241f 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -40,8 +40,8 @@ import (
 	"golang.org/x/image/draw"
 	"golang.org/x/net/idna"
 	"netlandish.com/x/gobwebs"
-	auditlog "netlandish.com/x/gobwebs-auditlog"
-	oauth2 "netlandish.com/x/gobwebs-oauth2"
+	"netlandish.com/x/gobwebs-auditlog"
+	"netlandish.com/x/gobwebs-oauth2"
 	gaccounts "netlandish.com/x/gobwebs/accounts"
 	gcore "netlandish.com/x/gobwebs/core"
 	"netlandish.com/x/gobwebs/crypto"
@@ -548,6 +548,22 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput)
 		return nil, err
 	}
 
+	if org.Visibility == models.VisibilityHidden &&
+		org.Settings.VisibilityOverride != "FORCED" &&
+		org.Settings.Billing.Status == models.BillingStatusFree {
+		opts := &database.FilterOptions{
+			Filter: sq.Eq{"org_id": org.ID},
+		}
+		linkCount, err := models.GetOrgLinksCount(ctx, opts)
+		if err == nil && linkCount >= 4 {
+			org.Visibility = models.VisibilityPublic
+			err = org.Store(ctx)
+			if err != nil {
+				srv.Logger().Printf("Failed to auto-promote org to PUBLIC: %v (org_id: %d)", err, org.ID)
+			}
+		}
+	}
+
 	if input.Archive {
 		srv.QueueTask("general", core.ArchiveURLSnapshotTask(srv, OrgLink))
 	}
@@ -1590,11 +1606,12 @@ func (r *mutationResolver) Register(ctx context.Context, input *model.RegisterIn
 	}
 
 	org := &models.Organization{
-		OwnerID:  int(user.ID),
-		OrgType:  models.OrgTypeUser,
-		Name:     user.Name,
-		Slug:     slug,
-		IsActive: true,
+		OwnerID:    int(user.ID),
+		OrgType:    models.OrgTypeUser,
+		Name:       user.Name,
+		Slug:       slug,
+		IsActive:   true,
+		Visibility: models.VisibilityHidden,
 		Settings: models.OrganizationSettings{
 			DefaultPerm: models.OrgLinkVisibilityPublic,
 			Billing: models.BillingSettings{
@@ -4422,6 +4439,12 @@ func (r *mutationResolver) UpdateAdminOrg(ctx context.Context, input model.Admin
 			return nil, nil
 		}
 		org.Visibility = vis
+
+		if vis == models.VisibilityHidden {
+			org.Settings.VisibilityOverride = "FORCED"
+		} else {
+			org.Settings.VisibilityOverride = ""
+		}
 		hasChanges = true
 	}
 
@@ -4984,7 +5007,7 @@ func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
 	return &model.Version{
 		Major:           0,
 		Minor:           7,
-		Patch:           0,
+		Patch:           1,
 		DeprecationDate: nil,
 	}, nil
 }
diff --git a/billing/processors.go b/billing/processors.go
index c75448c..d73a599 100644
--- a/billing/processors.go
+++ b/billing/processors.go
@@ -92,6 +92,10 @@ func ProcessCheckoutSessionCompletedTask(ctx context.Context, conf *config.Confi
 		org.IsActive = true
 	}
 
+	if org.Visibility == models.VisibilityHidden && org.Settings.VisibilityOverride != "FORCED" {
+		org.Visibility = models.VisibilityPublic
+	}
+
 	err = org.Store(ctx)
 	if err != nil {
 		return err
diff --git a/cmd/migrations.go b/cmd/migrations.go
index 892605c..77ed113 100644
--- a/cmd/migrations.go
+++ b/cmd/migrations.go
@@ -73,5 +73,12 @@ func GetMigrations() []migrate.Migration {
 			0,
 			links.MigrateFS,
 		),
+		migrate.FSFileMigration(
+			"0008_change_org_default_visibility",
+			"migrations/0008_change_org_default_visibility.up.sql",
+			"migrations/0008_change_org_default_visibility.down.sql",
+			0,
+			links.MigrateFS,
+		),
 	}
 }
diff --git a/migrations/0008_change_org_default_visibility.down.sql b/migrations/0008_change_org_default_visibility.down.sql
new file mode 100644
index 0000000..3480d66
--- /dev/null
+++ b/migrations/0008_change_org_default_visibility.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE organizations ALTER COLUMN visibility SET DEFAULT 'PUBLIC';
+
diff --git a/migrations/0008_change_org_default_visibility.up.sql b/migrations/0008_change_org_default_visibility.up.sql
new file mode 100644
index 0000000..e2ae9c5
--- /dev/null
+++ b/migrations/0008_change_org_default_visibility.up.sql
@@ -0,0 +1,8 @@
+-- Set visibilityOverride for existing HIDDEN organizations (using camelCase)
+UPDATE organizations 
+SET settings = jsonb_set(settings, '{visibilityOverride}', '"FORCED"'::jsonb)
+WHERE visibility = 'HIDDEN';
+
+-- Change default visibility for new organizations
+ALTER TABLE organizations ALTER COLUMN visibility SET DEFAULT 'HIDDEN';
+
diff --git a/models/organization.go b/models/organization.go
index 551d308..61cf9eb 100644
--- a/models/organization.go
+++ b/models/organization.go
@@ -50,8 +50,9 @@ type BillingSettings struct {
 
 // OrganizationSettings ...
 type OrganizationSettings struct {
-	DefaultPerm string          `json:"default_perm"` // default: OrgLinkVisibilityPublic
-	Billing     BillingSettings `json:"billing"`
+	DefaultPerm        string          `json:"default_perm"` // default: OrgLinkVisibilityPublic
+	Billing            BillingSettings `json:"billing"`
+	VisibilityOverride string          `json:"visibilityOverride,omitempty"` // "FORCED" when manually set to HIDDEN
 }
 
 // Value ...
@@ -173,9 +174,9 @@ func (o *Organization) Store(ctx context.Context) error {
 			err = sq.
 				Insert("organizations").
 				Columns("owner_id", "org_type", "name", "slug", "image", "timezone",
-					"settings", "is_active").
+					"settings", "is_active", "visibility").
 				Values(o.OwnerID, o.OrgType, o.Name, o.Slug, o.Image, o.Timezone,
-					settings, o.IsActive).
+					settings, o.IsActive, o.Visibility).
 				Suffix(`RETURNING id, created_on, updated_on`).
 				PlaceholderFormat(database.GetPlaceholderFormat()).
 				RunWith(tx).
@@ -359,7 +360,7 @@ func (o *Organization) TagCloud(ctx context.Context, order TagCloudOrdering) ([]
 // DeleteTagForService expects service value to be verified and tag to be provided.
 func (o *Organization) DeleteTagForService(ctx context.Context, service string, tag *Tag) error {
 	var junctionTable, parentTable, foreignKey string
-	
+
 	switch service {
 	case DomainServiceLinks:
 		junctionTable = "tag_links"
@@ -376,7 +377,7 @@ func (o *Organization) DeleteTagForService(ctx context.Context, service string,
 	default:
 		return fmt.Errorf("Invalid service type")
 	}
-	
+
 	whereClause := sq.And{
 		sq.Eq{junctionTable + ".tag_id": tag.ID},
 		sq.Expr(fmt.Sprintf(
@@ -384,7 +385,7 @@ func (o *Organization) DeleteTagForService(ctx context.Context, service string,
 			parentTable, parentTable, junctionTable, foreignKey, parentTable,
 		), o.ID),
 	}
-	
+
 	return database.WithTx(ctx, nil, func(tx *sql.Tx) error {
 		_, err := sq.Delete(junctionTable).
 			Where(whereClause).
@@ -399,7 +400,7 @@ func (o *Organization) DeleteTagForService(ctx context.Context, service string,
 func (o *Organization) RenameTagForService(ctx context.Context,
 	service string, oTag, nTag *Tag) error {
 	var junctionTable, parentTable, foreignKey string
-	
+
 	switch service {
 	case DomainServiceLinks:
 		junctionTable = "tag_links"
@@ -416,7 +417,7 @@ func (o *Organization) RenameTagForService(ctx context.Context,
 	default:
 		return fmt.Errorf("Invalid service type")
 	}
-	
+
 	whereClause := sq.And{
 		sq.Eq{junctionTable + ".tag_id": oTag.ID},
 		sq.Expr(fmt.Sprintf(
@@ -424,7 +425,7 @@ func (o *Organization) RenameTagForService(ctx context.Context,
 			parentTable, parentTable, junctionTable, foreignKey, parentTable,
 		), o.ID),
 	}
-	
+
 	return database.WithTx(ctx, nil, func(tx *sql.Tx) error {
 		_, err := sq.Update(junctionTable).
 			Set("tag_id", nTag.ID).
diff --git a/models/schema.sql b/models/schema.sql
index c01f32f..31b2c92 100644
--- a/models/schema.sql
+++ b/models/schema.sql
@@ -106,7 +106,7 @@ CREATE TABLE organizations (
   image VARCHAR(1024) DEFAULT '',
   settings JSONB DEFAULT '{}',
   timezone VARCHAR(20) NOT NULL DEFAULT 'UTC',
-  visibility public_visibility DEFAULT 'PUBLIC',
+  visibility public_visibility DEFAULT 'HIDDEN',
   is_active BOOLEAN DEFAULT TRUE,
   created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
   updated_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
diff --git a/templates/admin_org_detail.html b/templates/admin_org_detail.html
index 6997b9f..686f60a 100644
--- a/templates/admin_org_detail.html
+++ b/templates/admin_org_detail.html
@@ -33,7 +33,7 @@
       </tr>
       <tr>
         <th>{{.pd.Data.visibility}}</th>
-        <td>{{.org.Visibility}}</td>
+        <td>{{.org.Visibility}}{{ if eq .org.Settings.VisibilityOverride "FORCED" }} - <strong>({{ .pd.Data.forced }})</strong>{{ end }}</td>
       </tr>
       <tr>
         <th>{{.pd.Data.is_active}}</th>
-- 
2.49.1